X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2Fresourceloader%2FResourceLoaderModule.php;h=874eb666946370511c3bbbfeabb6fc500de72925;hb=f74244cd13961298e871ab5c9407b4d327c765b1;hp=c4041a419109039326d818e0694122689d7a7441;hpb=5161541d87d663ea86b66258a568349a6cc8fde9;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index c4041a4191..874eb66694 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -62,6 +62,14 @@ abstract class ResourceLoaderModule { protected $fileDeps = array(); // In-object cache for message blob mtime 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 + protected $isPositionDefined = false; /** * @var Config @@ -283,6 +291,19 @@ abstract class ResourceLoaderModule { return 'bottom'; } + /** + * 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. + * + * @return bool + * @since 1.26 + */ + public function isPositionDefault() { + return !$this->isPositionDefined; + } + /** * Whether this module's JS expects to work without the client-side ResourceLoader module. * Returning true from this function will prevent mw.loader.state() call from being @@ -312,9 +333,14 @@ abstract class ResourceLoaderModule { * * 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(); } @@ -384,8 +410,7 @@ abstract class ResourceLoaderModule { } /** - * Get the last modification timestamp of the message blob for this - * module in a given language. + * Get the last modification timestamp of the messages in this module for a given language. * @param string $lang Language code * @return int UNIX timestamp */ @@ -421,149 +446,280 @@ abstract class ResourceLoaderModule { $this->msgBlobMtime[$lang] = $mtime; } - /* Abstract Methods */ - /** - * Get this module's last modification timestamp for a given - * combination of language, skin and debug mode flag. This is typically - * the highest of each of the relevant components' modification - * timestamps. Whenever anything happens that changes the module's - * contents for these parameters, the mtime should increase. - * - * NOTE: The mtime of the module's messages is NOT automatically included. - * If you want this to happen, you'll need to call getMsgBlobMtime() - * yourself and take its result into consideration. + * Get an array of this module's resources. Ready for serving to the web. * - * NOTE: The mtime of the module's hash is NOT automatically included. - * If your module provides a getModifiedHash() method, you'll need to call getHashMtime() - * yourself and take its result into consideration. - * - * @param ResourceLoaderContext $context Context object - * @return int UNIX timestamp - */ - public function getModifiedTime( ResourceLoaderContext $context ) { - return 1; + * @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 ]; } /** - * Helper method for calculating when the module's hash (if it has one) changed. + * Bundle all resources attached to this module into an array. * + * @since 1.26 * @param ResourceLoaderContext $context - * @return int UNIX timestamp - */ - public function getHashMtime( ResourceLoaderContext $context ) { - $hash = $this->getModifiedHash( $context ); - if ( !is_string( $hash ) ) { - return 1; + * @return array + */ + final protected function buildContent( ResourceLoaderContext $context ) { + $rl = $context->getResourceLoader(); + + // 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; } - // Embed the hash itself in the cache key. This allows for a few nifty things: - // - During deployment, servers with old and new versions of the code communicating - // with the same memcached will not override the same key repeatedly increasing - // the timestamp. - // - In case of the definition changing and then changing back in a short period of time - // (e.g. in case of a revert or a corrupt server) the old timestamp and client-side cache - // url will be re-used. - // - If different context-combinations (e.g. same skin, same language or some combination - // thereof) result in the same definition, they will use the same hash and timestamp. - $cache = wfGetCache( CACHE_ANYTHING ); - $key = wfMemcKey( 'resourceloader', 'hashmtime', $this->getName(), $hash ); - - $data = $cache->get( $key ); - if ( is_int( $data ) && $data > 0 ) { - // We've seen this hash before, re-use the timestamp of when we first saw it. - return $data; + // 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; } - $timestamp = time(); - $cache->set( $key, $timestamp ); - return $timestamp; + return $content; } /** - * Get the hash for whatever this module may contain. + * Get a string identifying the current version of this module in a given context. * - * This is the method subclasses should implement if they want to make - * use of getHashMTime() inside getModifiedTime(). + * Whenever anything happens that changes the module's response (e.g. scripts, styles, and + * messages) this value must change. This value is used to store module responses in cache. + * (Both client-side and server-side.) * - * @param ResourceLoaderContext $context - * @return string|null Hash - */ - public function getModifiedHash( ResourceLoaderContext $context ) { - return null; - } - - /** - * Helper method for calculating when this module's definition summary was last changed. + * It is not recommended to override this directly. Use getDefinitionSummary() instead. + * If overridden, one must call the parent getVersionHash(), append data and re-hash. * - * @since 1.23 + * This method should be quick because it is frequently run by ResourceLoaderStartUpModule to + * propagate changes to the client and effectively invalidate cache. * + * For backward-compatibility, the following optional data providers are automatically included: + * + * - getModifiedTime() + * - getModifiedHash() + * + * @since 1.26 * @param ResourceLoaderContext $context - * @return int UNIX timestamp + * @return string Hash (should use ResourceLoader::makeHash) */ - public function getDefinitionMtime( ResourceLoaderContext $context ) { - $summary = $this->getDefinitionSummary( $context ); - if ( $summary === null ) { - return 1; - } + public function getVersionHash( ResourceLoaderContext $context ) { + // 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 ) ) { - $hash = md5( json_encode( $summary ) ); - $cache = wfGetCache( CACHE_ANYTHING ); - $key = wfMemcKey( 'resourceloader', 'moduledefinition', $this->getName(), $hash ); + $summary = $this->getDefinitionSummary( $context ); + if ( !isset( $summary['_cacheEpoch'] ) ) { + throw new Exception( 'getDefinitionSummary must call parent method' ); + } + $str = json_encode( $summary ); - $data = $cache->get( $key ); - if ( is_int( $data ) && $data > 0 ) { - // We've seen this hash before, re-use the timestamp of when we first saw it. - return $data; - } + $mtime = $this->getModifiedTime( $context ); + if ( $mtime !== null ) { + // Support: MediaWiki 1.25 and earlier + $str .= strval( $mtime ); + } - wfDebugLog( 'resourceloader', __METHOD__ . ": New definition for module " - . "{$this->getName()} in context \"{$context->getHash()}\"" ); - // WMF logging for T94810 - global $wgRequest; - if ( isset( $wgRequest ) && $context->getUser() ) { - wfDebugLog( 'resourceloader', __METHOD__ . ": Request with user parameter in " - . "context \"{$context->getHash()}\" from " . $wgRequest->getRequestURL() ); - } + $mhash = $this->getModifiedHash( $context ); + if ( $mhash !== null ) { + // Support: MediaWiki 1.25 and earlier + $str .= strval( $mhash ); + } - $timestamp = time(); - $cache->set( $key, $timestamp ); - return $timestamp; + $this->versionHash[ $contextHash ] = ResourceLoader::makeHash( $str ); + } + return $this->versionHash[ $contextHash ]; } /** * Get the definition summary for this module. * - * This is the method subclasses should implement if they want to make - * use of getDefinitionMTime() inside getModifiedTime(). + * This is the method subclasses are recommended to use to track values in their + * version hash. Call this in getVersionHash() and pass it to e.g. json_encode. + * + * Subclasses must call the parent getDefinitionSummary() and build on that. + * It is recommended that each subclass appends its own new array. This prevents + * clashes or accidental overwrites of existing keys and gives each subclass + * its own scope for simple array keys. + * + * @code + * $summary = parent::getDefinitionSummary( $context ); + * $summary[] = array( + * 'foo' => 123, + * 'bar' => 'quux', + * ); + * return $summary; + * @endcode * * Return an array containing values from all significant properties of this - * module's definition. Be sure to include things that are explicitly ordered, - * in their actaul order (bug 37812). + * module's definition. * - * Avoid including things that are insiginificant (e.g. order of message - * keys is insignificant and should be sorted to avoid unnecessary cache - * invalidation). + * Be careful not to normalise too much. Especially preserve the order of things + * that carry significance in getScript and getStyles (T39812). * - * Avoid including things already considered by other methods inside your - * getModifiedTime(), such as file mtime timestamps. + * Avoid including things that are insiginificant (e.g. order of message keys is + * insignificant and should be sorted to avoid unnecessary cache invalidation). * - * Serialisation is done using json_encode, which means object state is not - * taken into account when building the hash. This data structure must only - * contain arrays and scalars as values (avoid object instances) which means - * it requires abstraction. + * This data structure must exclusively contain arrays and scalars as values (avoid + * object instances) to allow simple serialisation using json_encode. * - * @since 1.23 + * If modules have a hash or timestamp from another source, that may be incuded as-is. * + * A number of utility methods are available to help you gather data. These are not + * called by default and must be included by the subclass' getDefinitionSummary(). + * + * - getMsgBlobMtime() + * + * @since 1.23 * @param ResourceLoaderContext $context * @return array|null */ public function getDefinitionSummary( ResourceLoaderContext $context ) { return array( - 'class' => get_class( $this ), + '_class' => get_class( $this ), + '_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ), ); } + /** + * Get this module's last modification timestamp for a given context. + * + * @deprecated since 1.26 Use getDefinitionSummary() instead + * @param ResourceLoaderContext $context Context object + * @return int|null UNIX timestamp + */ + public function getModifiedTime( ResourceLoaderContext $context ) { + return null; + } + + /** + * Helper method for providing a version hash to getVersionHash(). + * + * @deprecated since 1.26 Use getDefinitionSummary() instead + * @param ResourceLoaderContext $context + * @return string|null Hash + */ + public function getModifiedHash( ResourceLoaderContext $context ) { + return null; + } + + /** + * Back-compat dummy for old subclass implementations of getModifiedTime(). + * + * This method used to use ObjectCache to track when a hash was first seen. That principle + * stems from a time that ResourceLoader could only identify module versions by timestamp. + * That is no longer the case. Use getDefinitionSummary() directly. + * + * @deprecated since 1.26 Superseded by getVersionHash() + * @param ResourceLoaderContext $context + * @return int UNIX timestamp + */ + public function getHashMtime( ResourceLoaderContext $context ) { + if ( !is_string( $this->getModifiedHash( $context ) ) ) { + return 1; + } + // Dummy that is > 1 + return 2; + } + + /** + * Back-compat dummy for old subclass implementations of getModifiedTime(). + * + * @since 1.23 + * @deprecated since 1.26 Superseded by getVersionHash() + * @param ResourceLoaderContext $context + * @return int UNIX timestamp + */ + public function getDefinitionMtime( ResourceLoaderContext $context ) { + if ( $this->getDefinitionSummary( $context ) === null ) { + return 1; + } + // Dummy that is > 1 + return 2; + } + /** * Check whether this module is known to be empty. If a child class * has an easy and cheap way to determine that this module is @@ -607,7 +763,7 @@ abstract class ResourceLoaderModule { } catch ( Exception $e ) { // We'll save this to cache to avoid having to validate broken JS over and over... $err = $e->getMessage(); - $result = "throw new Error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");"; + $result = "mw.log.error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");"; } $cache->set( $key, $result );