protected $msgBlobMtime = array();
// In-object cache for version hash
protected $versionHash = array();
+ // In-object cache for module content
+ protected $contents = array();
// Whether the position returned by getPosition() is defined in the module configuration
// and not a default value
/**
* Whether the position returned by getPosition() is a default value or comes from the module
- * definition. This method is meant to be short-lived, and is only useful until classes added via
- * addModuleStyles with a default value define an explicit position. See getModuleStyles() in
- * OutputPage for the related migration warning.
+ * definition. This method is meant to be short-lived, and is only useful until classes added
+ * via addModuleStyles with a default value define an explicit position. See getModuleStyles()
+ * in OutputPage for the related migration warning.
*
* @return bool
* @since 1.26
*
* To add dependencies dynamically on the client side, use a custom
* loader script, see getLoaderScript()
+ *
+ * Note: It is expected that $context will be made non-optional in the near
+ * future.
+ *
+ * @param ResourceLoaderContext $context
* @return array List of module names as strings
*/
- public function getDependencies() {
+ public function getDependencies( ResourceLoaderContext $context = null ) {
// Stub, override expected
return array();
}
}
$dbr = wfGetDB( DB_SLAVE );
- $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
+ $deps = $dbr->selectField( 'module_deps',
+ 'md_deps',
+ array(
'md_module' => $this->getName(),
'md_skin' => $skin,
- ), __METHOD__
+ ),
+ __METHOD__
);
+
if ( !is_null( $deps ) ) {
$this->fileDeps[$skin] = (array)FormatJson::decode( $deps, true );
} else {
$this->fileDeps[$skin] = array();
}
+
return $this->fileDeps[$skin];
}
}
$dbr = wfGetDB( DB_SLAVE );
- $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
+ $msgBlobMtime = $dbr->selectField( 'msg_resource',
+ 'mr_timestamp',
+ array(
'mr_resource' => $this->getName(),
'mr_lang' => $lang
- ), __METHOD__
+ ),
+ __METHOD__
);
// If no blob was found, but the module does have messages, that means we need
// to regenerate it. Return NOW
$this->msgBlobMtime[$lang] = $mtime;
}
+ /**
+ * Get an array of this module's resources. Ready for serving to the web.
+ *
+ * @since 1.26
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getModuleContent( ResourceLoaderContext $context ) {
+ $contextHash = $context->getHash();
+ // Cache this expensive operation. This calls builds the scripts, styles, and messages
+ // content which typically involves filesystem and/or database access.
+ if ( !array_key_exists( $contextHash, $this->contents ) ) {
+ $this->contents[$contextHash] = $this->buildContent( $context );
+ }
+ return $this->contents[$contextHash];
+ }
+
+ /**
+ * Bundle all resources attached to this module into an array.
+ *
+ * @since 1.26
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ final protected function buildContent( ResourceLoaderContext $context ) {
+ $rl = $context->getResourceLoader();
+ $stats = RequestContext::getMain()->getStats();
+ $statStart = microtime( true );
+
+ // Only include properties that are relevant to this context (e.g. only=scripts)
+ // and that are non-empty (e.g. don't include "templates" for modules without
+ // templates). This helps prevent invalidating cache for all modules when new
+ // optional properties are introduced.
+ $content = array();
+
+ // Scripts
+ if ( $context->shouldIncludeScripts() ) {
+ // If we are in debug mode, we'll want to return an array of URLs if possible
+ // However, we can't do this if the module doesn't support it
+ // We also can't do this if there is an only= parameter, because we have to give
+ // the module a way to return a load.php URL without causing an infinite loop
+ if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
+ $scripts = $this->getScriptURLsForDebug( $context );
+ } else {
+ $scripts = $this->getScript( $context );
+ // rtrim() because there are usually a few line breaks
+ // after the last ';'. A new line at EOF, a new line
+ // added by ResourceLoaderFileModule::readScriptFiles, etc.
+ if ( is_string( $scripts )
+ && strlen( $scripts )
+ && substr( rtrim( $scripts ), -1 ) !== ';'
+ ) {
+ // Append semicolon to prevent weird bugs caused by files not
+ // terminating their statements right (bug 27054)
+ $scripts .= ";\n";
+ }
+ }
+ $content['scripts'] = $scripts;
+ }
+
+ // Styles
+ if ( $context->shouldIncludeStyles() ) {
+ $styles = array();
+ // Don't create empty stylesheets like array( '' => '' ) for modules
+ // that don't *have* any stylesheets (bug 38024).
+ $stylePairs = $this->getStyles( $context );
+ if ( count( $stylePairs ) ) {
+ // If we are in debug mode without &only= set, we'll want to return an array of URLs
+ // See comment near shouldIncludeScripts() for more details
+ if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
+ $styles = array(
+ 'url' => $this->getStyleURLsForDebug( $context )
+ );
+ } else {
+ // Minify CSS before embedding in mw.loader.implement call
+ // (unless in debug mode)
+ if ( !$context->getDebug() ) {
+ foreach ( $stylePairs as $media => $style ) {
+ // Can be either a string or an array of strings.
+ if ( is_array( $style ) ) {
+ $stylePairs[$media] = array();
+ foreach ( $style as $cssText ) {
+ if ( is_string( $cssText ) ) {
+ $stylePairs[$media][] =
+ $rl->filter( 'minify-css', $cssText );
+ }
+ }
+ } elseif ( is_string( $style ) ) {
+ $stylePairs[$media] = $rl->filter( 'minify-css', $style );
+ }
+ }
+ }
+ // Wrap styles into @media groups as needed and flatten into a numerical array
+ $styles = array(
+ 'css' => $rl->makeCombinedStyles( $stylePairs )
+ );
+ }
+ }
+ $content['styles'] = $styles;
+ }
+
+ // Messages
+ $blobs = $rl->getMessageBlobStore()->get(
+ $rl,
+ array( $this->getName() => $this ),
+ $context->getLanguage()
+ );
+ if ( isset( $blobs[$this->getName()] ) ) {
+ $content['messagesBlob'] = $blobs[$this->getName()];
+ }
+
+ $templates = $this->getTemplates();
+ if ( $templates ) {
+ $content['templates'] = $templates;
+ }
+
+ $statTiming = microtime( true ) - $statStart;
+ $statName = str_replace( '.', '_', $this->getName() );
+ $stats->timing( "resourceloader_build.all", $statTiming );
+ $stats->timing( "resourceloader_build.$statName", $statTiming );
+
+ return $content;
+ }
+
/**
* Get a string identifying the current version of this module in a given context.
*
* @return string Hash (should use ResourceLoader::makeHash)
*/
public function getVersionHash( ResourceLoaderContext $context ) {
+ // The startup module produces a manifest with versions representing the entire module.
+ // Typically, the request for the startup module itself has only=scripts. That must apply
+ // only to the startup module content, and not to the module version computed here.
+ $context = new DerivativeResourceLoaderContext( $context );
+ $context->setModules( array() );
+ // Version hash must cover all resources, regardless of startup request itself.
+ $context->setOnly( null );
+ // Compute version hash based on content, not debug urls.
+ $context->setDebug( false );
+
// Cache this somewhat expensive operation. Especially because some classes
// (e.g. startup module) iterate more than once over all modules to get versions.
$contextHash = $context->getHash();
if ( !array_key_exists( $contextHash, $this->versionHash ) ) {
- $summary = $this->getDefinitionSummary( $context );
- if ( !isset( $summary['_cacheEpoch'] ) ) {
- throw new Exception( 'getDefinitionSummary must call parent method' );
- }
- $str = json_encode( $summary );
-
- $mtime = $this->getModifiedTime( $context );
- if ( $mtime !== null ) {
- // Support: MediaWiki 1.25 and earlier
- $str .= strval( $mtime );
+ if ( $this->enableModuleContentVersion() ) {
+ // Detect changes directly
+ $str = json_encode( $this->getModuleContent( $context ) );
+ } else {
+ // Infer changes based on definition and other metrics
+ $summary = $this->getDefinitionSummary( $context );
+ if ( !isset( $summary['_cacheEpoch'] ) ) {
+ throw new LogicException( 'getDefinitionSummary must call parent method' );
+ }
+ $str = json_encode( $summary );
+
+ $mtime = $this->getModifiedTime( $context );
+ if ( $mtime !== null ) {
+ // Support: MediaWiki 1.25 and earlier
+ $str .= strval( $mtime );
+ }
+
+ $mhash = $this->getModifiedHash( $context );
+ if ( $mhash !== null ) {
+ // Support: MediaWiki 1.25 and earlier
+ $str .= strval( $mhash );
+ }
}
- $mhash = $this->getModifiedHash( $context );
- if ( $mhash !== null ) {
- // Support: MediaWiki 1.25 and earlier
- $str .= strval( $mhash );
- }
-
- $this->versionHash[ $contextHash ] = ResourceLoader::makeHash( $str );
+ $this->versionHash[$contextHash] = ResourceLoader::makeHash( $str );
}
- return $this->versionHash[ $contextHash ];
+ return $this->versionHash[$contextHash];
+ }
+
+ /**
+ * Whether to generate version hash based on module content.
+ *
+ * If a module requires database or file system access to build the module
+ * content, consider disabling this in favour of manually tracking relevant
+ * aspects in getDefinitionSummary(). See getVersionHash() for how this is used.
+ *
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return false;
}
/**
protected function validateScriptFile( $fileName, $contents ) {
if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) {
// Try for cache hit
- // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
- $key = wfMemcKey( 'resourceloader', 'jsparse', self::$parseCacheVersion, md5( $contents ) );
+ // Use CACHE_ANYTHING since parsing JS is much slower than a DB query
+ $key = wfMemcKey(
+ 'resourceloader',
+ 'jsparse',
+ self::$parseCacheVersion,
+ md5( $contents )
+ );
$cache = wfGetCache( CACHE_ANYTHING );
$cacheEntry = $cache->get( $key );
if ( is_string( $cacheEntry ) ) {
} catch ( Exception $e ) {
// We'll save this to cache to avoid having to validate broken JS over and over...
$err = $e->getMessage();
- $result = "mw.log.error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
+ $result = "mw.log.error(" .
+ Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
}
$cache->set( $key, $result );
* @return int UNIX timestamp
*/
protected static function safeFilemtime( $filename ) {
- wfSuppressWarnings();
+ MediaWiki\suppressWarnings();
$mtime = filemtime( $filename ) ?: 1;
- wfRestoreWarnings();
+ MediaWiki\restoreWarnings();
return $mtime;
}