summaryrefslogtreecommitdiff
path: root/includes/resourceloader
diff options
context:
space:
mode:
authorPierre Schmitz <pierre@archlinux.de>2015-12-17 09:15:42 +0100
committerPierre Schmitz <pierre@archlinux.de>2015-12-17 09:44:51 +0100
commita1789ddde42033f1b05cc4929491214ee6e79383 (patch)
tree63615735c4ddffaaabf2428946bb26f90899f7bf /includes/resourceloader
parent9e06a62f265e3a2aaabecc598d4bc617e06fa32d (diff)
Update to MediaWiki 1.26.0
Diffstat (limited to 'includes/resourceloader')
-rw-r--r--includes/resourceloader/DerivativeResourceLoaderContext.php76
-rw-r--r--includes/resourceloader/ResourceLoader.php553
-rw-r--r--includes/resourceloader/ResourceLoaderContext.php33
-rw-r--r--includes/resourceloader/ResourceLoaderEditToolbarModule.php31
-rw-r--r--includes/resourceloader/ResourceLoaderFileModule.php122
-rw-r--r--includes/resourceloader/ResourceLoaderForeignApiModule.php33
-rw-r--r--includes/resourceloader/ResourceLoaderImage.php25
-rw-r--r--includes/resourceloader/ResourceLoaderImageModule.php193
-rw-r--r--includes/resourceloader/ResourceLoaderJqueryMsgModule.php66
-rw-r--r--includes/resourceloader/ResourceLoaderLanguageDataModule.php16
-rw-r--r--includes/resourceloader/ResourceLoaderLanguageNamesModule.php18
-rw-r--r--includes/resourceloader/ResourceLoaderModule.php479
-rw-r--r--includes/resourceloader/ResourceLoaderOOUIImageModule.php86
-rw-r--r--includes/resourceloader/ResourceLoaderRawFileModule.php52
-rw-r--r--includes/resourceloader/ResourceLoaderSiteModule.php9
-rw-r--r--includes/resourceloader/ResourceLoaderSkinModule.php13
-rw-r--r--includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php19
-rw-r--r--includes/resourceloader/ResourceLoaderStartUpModule.php191
-rw-r--r--includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php18
-rw-r--r--includes/resourceloader/ResourceLoaderUserDefaultsModule.php19
-rw-r--r--includes/resourceloader/ResourceLoaderUserOptionsModule.php21
-rw-r--r--includes/resourceloader/ResourceLoaderWikiModule.php198
22 files changed, 1424 insertions, 847 deletions
diff --git a/includes/resourceloader/DerivativeResourceLoaderContext.php b/includes/resourceloader/DerivativeResourceLoaderContext.php
index 5784f2a0..59675372 100644
--- a/includes/resourceloader/DerivativeResourceLoaderContext.php
+++ b/includes/resourceloader/DerivativeResourceLoaderContext.php
@@ -28,32 +28,32 @@
* @since 1.24
*/
class DerivativeResourceLoaderContext extends ResourceLoaderContext {
+ const INHERIT_VALUE = -1;
/**
* @var ResourceLoaderContext
*/
private $context;
- protected $modules;
- protected $language;
- protected $direction;
- protected $skin;
- protected $user;
- protected $debug;
- protected $only;
- protected $version;
- protected $hash;
- protected $raw;
+
+ protected $modules = self::INHERIT_VALUE;
+ protected $language = self::INHERIT_VALUE;
+ protected $direction = self::INHERIT_VALUE;
+ protected $skin = self::INHERIT_VALUE;
+ protected $user = self::INHERIT_VALUE;
+ protected $debug = self::INHERIT_VALUE;
+ protected $only = self::INHERIT_VALUE;
+ protected $version = self::INHERIT_VALUE;
+ protected $raw = self::INHERIT_VALUE;
public function __construct( ResourceLoaderContext $context ) {
$this->context = $context;
}
public function getModules() {
- if ( !is_null( $this->modules ) ) {
- return $this->modules;
- } else {
+ if ( $this->modules === self::INHERIT_VALUE ) {
return $this->context->getModules();
}
+ return $this->modules;
}
/**
@@ -64,11 +64,10 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
}
public function getLanguage() {
- if ( !is_null( $this->language ) ) {
- return $this->language;
- } else {
+ if ( $this->language === self::INHERIT_VALUE ) {
return $this->context->getLanguage();
}
+ return $this->language;
}
/**
@@ -76,16 +75,19 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
*/
public function setLanguage( $language ) {
$this->language = $language;
- $this->direction = null; // Invalidate direction since it might be based on language
+ // Invalidate direction since it is based on language
+ $this->direction = null;
$this->hash = null;
}
public function getDirection() {
- if ( !is_null( $this->direction ) ) {
- return $this->direction;
- } else {
+ if ( $this->direction === self::INHERIT_VALUE ) {
return $this->context->getDirection();
}
+ if ( $this->direction === null ) {
+ $this->direction = Language::factory( $this->getLanguage() )->getDir();
+ }
+ return $this->direction;
}
/**
@@ -97,11 +99,10 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
}
public function getSkin() {
- if ( !is_null( $this->skin ) ) {
- return $this->skin;
- } else {
+ if ( $this->skin === self::INHERIT_VALUE ) {
return $this->context->getSkin();
}
+ return $this->skin;
}
/**
@@ -113,11 +114,10 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
}
public function getUser() {
- if ( !is_null( $this->user ) ) {
- return $this->user;
- } else {
+ if ( $this->user === self::INHERIT_VALUE ) {
return $this->context->getUser();
}
+ return $this->user;
}
/**
@@ -130,11 +130,10 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
}
public function getDebug() {
- if ( !is_null( $this->debug ) ) {
- return $this->debug;
- } else {
+ if ( $this->debug === self::INHERIT_VALUE ) {
return $this->context->getDebug();
}
+ return $this->debug;
}
/**
@@ -146,15 +145,14 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
}
public function getOnly() {
- if ( !is_null( $this->only ) ) {
- return $this->only;
- } else {
+ if ( $this->only === self::INHERIT_VALUE ) {
return $this->context->getOnly();
}
+ return $this->only;
}
/**
- * @param string $only
+ * @param string|null $only
*/
public function setOnly( $only ) {
$this->only = $only;
@@ -162,15 +160,14 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
}
public function getVersion() {
- if ( !is_null( $this->version ) ) {
- return $this->version;
- } else {
+ if ( $this->version === self::INHERIT_VALUE ) {
return $this->context->getVersion();
}
+ return $this->version;
}
/**
- * @param string $version
+ * @param string|null $version
*/
public function setVersion( $version ) {
$this->version = $version;
@@ -178,11 +175,10 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext {
}
public function getRaw() {
- if ( !is_null( $this->raw ) ) {
- return $this->raw;
- } else {
+ if ( $this->raw === self::INHERIT_VALUE ) {
return $this->context->getRaw();
}
+ return $this->raw;
}
/**
diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php
index 150ccd07..c8ece147 100644
--- a/includes/resourceloader/ResourceLoader.php
+++ b/includes/resourceloader/ResourceLoader.php
@@ -22,13 +22,18 @@
* @author Trevor Parscal
*/
+use Psr\Log\LoggerAwareInterface;
+use Psr\Log\LoggerInterface;
+use Psr\Log\NullLogger;
+use WrappedString\WrappedString;
+
/**
* Dynamic JavaScript and CSS resource loading system.
*
* Most of the documentation is on the MediaWiki documentation wiki starting at:
* https://www.mediawiki.org/wiki/ResourceLoader
*/
-class ResourceLoader {
+class ResourceLoader implements LoggerAwareInterface {
/** @var int */
protected static $filterCacheVersion = 7;
@@ -78,6 +83,11 @@ class ResourceLoader {
protected $blobStore;
/**
+ * @var LoggerInterface
+ */
+ private $logger;
+
+ /**
* Load information stored in the database about modules.
*
* This method grabs modules dependencies from the database and updates modules
@@ -169,74 +179,98 @@ class ResourceLoader {
*
* @param string $filter Name of filter to run
* @param string $data Text to filter, such as JavaScript or CSS text
- * @param string $cacheReport Whether to include the cache key report
+ * @param array $options For back-compat, can also be the boolean value for "cacheReport". Keys:
+ * - (bool) cache: Whether to allow caching this data. Default: true.
+ * - (bool) cacheReport: Whether to include the "cache key" report comment. Default: false.
* @return string Filtered data, or a comment containing an error message
*/
- public function filter( $filter, $data, $cacheReport = true ) {
+ public function filter( $filter, $data, $options = array() ) {
+ // Back-compat
+ if ( is_bool( $options ) ) {
+ $options = array( 'cacheReport' => $options );
+ }
+ // Defaults
+ $options += array( 'cache' => true, 'cacheReport' => false );
+ $stats = RequestContext::getMain()->getStats();
- // For empty/whitespace-only data or for unknown filters, don't perform
- // any caching or processing
- if ( trim( $data ) === '' || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) {
+ // Don't filter empty content
+ if ( trim( $data ) === '' ) {
return $data;
}
- // Try for cache hit
- // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
- $key = wfMemcKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) );
- $cache = wfGetCache( CACHE_ANYTHING );
- $cacheEntry = $cache->get( $key );
- if ( is_string( $cacheEntry ) ) {
- wfIncrStats( "rl-$filter-cache-hits" );
- return $cacheEntry;
+ if ( !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) {
+ $this->logger->warning( 'Invalid filter {filter}', array(
+ 'filter' => $filter
+ ) );
+ return $data;
}
- $result = '';
- // Run the filter - we've already verified one of these will work
- try {
- wfIncrStats( "rl-$filter-cache-misses" );
- switch ( $filter ) {
- case 'minify-js':
- $result = JavaScriptMinifier::minify( $data,
- $this->config->get( 'ResourceLoaderMinifierStatementsOnOwnLine' ),
- $this->config->get( 'ResourceLoaderMinifierMaxLineLength' )
- );
- if ( $cacheReport ) {
- $result .= "\n/* cache key: $key */";
- }
- break;
- case 'minify-css':
- $result = CSSMin::minify( $data );
- if ( $cacheReport ) {
- $result .= "\n/* cache key: $key */";
- }
- break;
+ if ( !$options['cache'] ) {
+ $result = self::applyFilter( $filter, $data, $this->config );
+ } else {
+ $key = wfGlobalCacheKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) );
+ $cache = ObjectCache::newAccelerator( CACHE_ANYTHING );
+ $cacheEntry = $cache->get( $key );
+ if ( is_string( $cacheEntry ) ) {
+ $stats->increment( "resourceloader_cache.$filter.hit" );
+ return $cacheEntry;
+ }
+ $result = '';
+ try {
+ $statStart = microtime( true );
+ $result = self::applyFilter( $filter, $data, $this->config );
+ $statTiming = microtime( true ) - $statStart;
+ $stats->increment( "resourceloader_cache.$filter.miss" );
+ $stats->timing( "resourceloader_cache.$filter.timing", 1000 * $statTiming );
+ if ( $options['cacheReport'] ) {
+ $result .= "\n/* cache key: $key */";
+ }
+ // Set a TTL since HHVM's APC doesn't have any limitation or eviction logic.
+ $cache->set( $key, $result, 24 * 3600 );
+ } catch ( Exception $e ) {
+ MWExceptionHandler::logException( $e );
+ $this->logger->warning( 'Minification failed: {exception}', array(
+ 'exception' => $e
+ ) );
+ $this->errors[] = self::formatExceptionNoComment( $e );
}
-
- // Save filtered text to Memcached
- $cache->set( $key, $result );
- } catch ( Exception $e ) {
- MWExceptionHandler::logException( $e );
- wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" );
- $this->errors[] = self::formatExceptionNoComment( $e );
}
return $result;
}
+ private static function applyFilter( $filter, $data, Config $config ) {
+ switch ( $filter ) {
+ case 'minify-js':
+ return JavaScriptMinifier::minify( $data,
+ $config->get( 'ResourceLoaderMinifierStatementsOnOwnLine' ),
+ $config->get( 'ResourceLoaderMinifierMaxLineLength' )
+ );
+ case 'minify-css':
+ return CSSMin::minify( $data );
+ }
+
+ return $data;
+ }
+
/* Methods */
/**
* Register core modules and runs registration hooks.
* @param Config|null $config
*/
- public function __construct( Config $config = null ) {
+ public function __construct( Config $config = null, LoggerInterface $logger = null ) {
global $IP;
- if ( $config === null ) {
- wfDebug( __METHOD__ . ' was called without providing a Config instance' );
- $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+ if ( !$logger ) {
+ $logger = new NullLogger();
}
+ $this->setLogger( $logger );
+ if ( !$config ) {
+ $this->logger->debug( __METHOD__ . ' was called without providing a Config instance' );
+ $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
+ }
$this->config = $config;
// Add 'local' source first
@@ -247,9 +281,10 @@ class ResourceLoader {
// Register core modules
$this->register( include "$IP/resources/Resources.php" );
+ $this->register( include "$IP/resources/ResourcesOOUI.php" );
// Register extension modules
- Hooks::run( 'ResourceLoaderRegisterModules', array( &$this ) );
$this->register( $config->get( 'ResourceModules' ) );
+ Hooks::run( 'ResourceLoaderRegisterModules', array( &$this ) );
if ( $config->get( 'EnableJavaScriptTest' ) === true ) {
$this->registerTestModules();
@@ -265,9 +300,21 @@ class ResourceLoader {
return $this->config;
}
+ public function setLogger( LoggerInterface $logger ) {
+ $this->logger = $logger;
+ }
+
+ /**
+ * @since 1.26
+ * @return MessageBlobStore
+ */
+ public function getMessageBlobStore() {
+ return $this->blobStore;
+ }
+
/**
- * @param MessageBlobStore $blobStore
* @since 1.25
+ * @param MessageBlobStore $blobStore
*/
public function setMessageBlobStore( MessageBlobStore $blobStore ) {
$this->blobStore = $blobStore;
@@ -566,19 +613,44 @@ class ResourceLoader {
}
/**
+ * @since 1.26
+ * @param string $value
+ * @return string Hash
+ */
+ public static function makeHash( $value ) {
+ // Use base64 to output more entropy in a more compact string (default hex is only base16).
+ // The first 8 chars of a base64 encoded digest represent the same binary as
+ // the first 12 chars of a hex encoded digest.
+ return substr( base64_encode( sha1( $value, true ) ), 0, 8 );
+ }
+
+ /**
+ * Helper method to get and combine versions of multiple modules.
+ *
+ * @since 1.26
+ * @param ResourceLoaderContext $context
+ * @param array $modules List of ResourceLoaderModule objects
+ * @return string Hash
+ */
+ public function getCombinedVersion( ResourceLoaderContext $context, Array $modules ) {
+ if ( !$modules ) {
+ return '';
+ }
+ // Support: PHP 5.3 ("$this" for anonymous functions was added in PHP 5.4.0)
+ // http://php.net/functions.anonymous
+ $rl = $this;
+ $hashes = array_map( function ( $module ) use ( $rl, $context ) {
+ return $rl->getModule( $module )->getVersionHash( $context );
+ }, $modules );
+ return self::makeHash( implode( $hashes ) );
+ }
+
+ /**
* Output a response to a load request, including the content-type header.
*
* @param ResourceLoaderContext $context Context in which a response should be formed
*/
public function respond( ResourceLoaderContext $context ) {
- // Use file cache if enabled and available...
- if ( $this->config->get( 'UseFileCache' ) ) {
- $fileCache = ResourceFileCache::newFromContext( $context );
- if ( $this->tryRespondFromFileCache( $fileCache, $context ) ) {
- return; // output handled
- }
- }
-
// Buffer output to catch warnings. Normally we'd use ob_clean() on the
// top-level output buffer to clear warnings, but that breaks when ob_gzhandler
// is used: ob_clean() will clear the GZIP header in that case and it won't come
@@ -597,7 +669,7 @@ class ResourceLoader {
// Do not allow private modules to be loaded from the web.
// This is a security issue, see bug 34907.
if ( $module->getGroup() === 'private' ) {
- wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" );
+ $this->logger->debug( "Request for private module '$name' denied" );
$this->errors[] = "Cannot show private module \"$name\"";
continue;
}
@@ -607,37 +679,46 @@ class ResourceLoader {
}
}
- // Preload information needed to the mtime calculation below
try {
+ // Preload for getCombinedVersion()
$this->preloadModuleInfo( array_keys( $modules ), $context );
} catch ( Exception $e ) {
MWExceptionHandler::logException( $e );
- wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" );
+ $this->logger->warning( 'Preloading module info failed: {exception}', array(
+ 'exception' => $e
+ ) );
$this->errors[] = self::formatExceptionNoComment( $e );
}
- // To send Last-Modified and support If-Modified-Since, we need to detect
- // the last modified time
- $mtime = wfTimestamp( TS_UNIX, $this->config->get( 'CacheEpoch' ) );
- foreach ( $modules as $module ) {
- /**
- * @var $module ResourceLoaderModule
- */
- try {
- // Calculate maximum modified time
- $mtime = max( $mtime, $module->getModifiedTime( $context ) );
- } catch ( Exception $e ) {
- MWExceptionHandler::logException( $e );
- wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" );
- $this->errors[] = self::formatExceptionNoComment( $e );
- }
+ // Combine versions to propagate cache invalidation
+ $versionHash = '';
+ try {
+ $versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) );
+ } catch ( Exception $e ) {
+ MWExceptionHandler::logException( $e );
+ $this->logger->warning( 'Calculating version hash failed: {exception}', array(
+ 'exception' => $e
+ ) );
+ $this->errors[] = self::formatExceptionNoComment( $e );
}
- // If there's an If-Modified-Since header, respond with a 304 appropriately
- if ( $this->tryRespondLastModified( $context, $mtime ) ) {
+ // See RFC 2616 § 3.11 Entity Tags
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11
+ $etag = 'W/"' . $versionHash . '"';
+
+ // Try the client-side cache first
+ if ( $this->tryRespondNotModified( $context, $etag ) ) {
return; // output handled (buffers cleared)
}
+ // Use file cache if enabled and available...
+ if ( $this->config->get( 'UseFileCache' ) ) {
+ $fileCache = ResourceFileCache::newFromContext( $context );
+ if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
+ return; // output handled
+ }
+ }
+
// Generate a response
$response = $this->makeModuleResponse( $context, $modules, $missing );
@@ -659,26 +740,25 @@ class ResourceLoader {
}
}
- // Send content type and cache related headers
- $this->sendResponseHeaders( $context, $mtime, (bool)$this->errors );
+ $this->sendResponseHeaders( $context, $etag, (bool)$this->errors );
// Remove the output buffer and output the response
ob_end_clean();
if ( $context->getImageObj() && $this->errors ) {
// We can't show both the error messages and the response when it's an image.
- $errorText = '';
- foreach ( $this->errors as $error ) {
- $errorText .= $error . "\n";
- }
- $response = $errorText;
+ $response = implode( "\n\n", $this->errors );
} elseif ( $this->errors ) {
- // Prepend comments indicating errors
- $errorText = '';
- foreach ( $this->errors as $error ) {
- $errorText .= self::makeComment( $error );
+ $errorText = implode( "\n\n", $this->errors );
+ $errorResponse = self::makeComment( $errorText );
+ if ( $context->shouldIncludeScripts() ) {
+ $errorResponse .= 'if (window.console && console.error) {'
+ . Xml::encodeJsCall( 'console.error', array( $errorText ) )
+ . "}\n";
}
- $response = $errorText . $response;
+
+ // Prepend error info to the response
+ $response = $errorResponse . $response;
}
$this->errors = array();
@@ -687,13 +767,16 @@ class ResourceLoader {
}
/**
- * Send content type and last modified headers to the client.
+ * Send main response headers to the client.
+ *
+ * Deals with Content-Type, CORS (for stylesheets), and caching.
+ *
* @param ResourceLoaderContext $context
- * @param string $mtime TS_MW timestamp to use for last-modified
+ * @param string $etag ETag header value
* @param bool $errors Whether there are errors in the response
* @return void
*/
- protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) {
+ protected function sendResponseHeaders( ResourceLoaderContext $context, $etag, $errors ) {
$rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
// If a version wasn't specified we need a shorter expiry time for updates
// to propagate to clients quickly
@@ -720,7 +803,9 @@ class ResourceLoader {
} else {
header( 'Content-Type: text/javascript; charset=utf-8' );
}
- header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
+ // See RFC 2616 § 14.19 ETag
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19
+ header( 'ETag: ' . $etag );
if ( $context->getDebug() ) {
// Do not cache debug responses
header( 'Cache-Control: private, no-cache, must-revalidate' );
@@ -733,39 +818,36 @@ class ResourceLoader {
}
/**
- * Respond with 304 Last Modified if appropiate.
+ * Respond with HTTP 304 Not Modified if appropiate.
*
- * If there's an If-Modified-Since header, respond with a 304 appropriately
+ * If there's an If-None-Match header, respond with a 304 appropriately
* and clear out the output buffer. If the client cache is too old then do nothing.
*
* @param ResourceLoaderContext $context
- * @param string $mtime The TS_MW timestamp to check the header against
- * @return bool True if 304 header sent and output handled
+ * @param string $etag ETag header value
+ * @return bool True if HTTP 304 was sent and output handled
*/
- protected function tryRespondLastModified( ResourceLoaderContext $context, $mtime ) {
- // If there's an If-Modified-Since header, respond with a 304 appropriately
- // Some clients send "timestamp;length=123". Strip the part after the first ';'
- // so we get a valid timestamp.
- $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
+ protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) {
+ // See RFC 2616 § 14.26 If-None-Match
+ // http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26
+ $clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST );
// Never send 304s in debug mode
- if ( $ims !== false && !$context->getDebug() ) {
- $imsTS = strtok( $ims, ';' );
- if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) {
- // There's another bug in ob_gzhandler (see also the comment at
- // the top of this function) that causes it to gzip even empty
- // responses, meaning it's impossible to produce a truly empty
- // response (because the gzip header is always there). This is
- // a problem because 304 responses have to be completely empty
- // per the HTTP spec, and Firefox behaves buggily when they're not.
- // See also http://bugs.php.net/bug.php?id=51579
- // To work around this, we tear down all output buffering before
- // sending the 304.
- wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
-
- header( 'HTTP/1.0 304 Not Modified' );
- header( 'Status: 304 Not Modified' );
- return true;
- }
+ if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) {
+ // There's another bug in ob_gzhandler (see also the comment at
+ // the top of this function) that causes it to gzip even empty
+ // responses, meaning it's impossible to produce a truly empty
+ // response (because the gzip header is always there). This is
+ // a problem because 304 responses have to be completely empty
+ // per the HTTP spec, and Firefox behaves buggily when they're not.
+ // See also http://bugs.php.net/bug.php?id=51579
+ // To work around this, we tear down all output buffering before
+ // sending the 304.
+ wfResetOutputBuffers( /* $resetGzipEncoding = */ true );
+
+ HttpStatus::header( 304 );
+
+ $this->sendResponseHeaders( $context, $etag, false );
+ return true;
}
return false;
}
@@ -775,10 +857,13 @@ class ResourceLoader {
*
* @param ResourceFileCache $fileCache Cache object for this request URL
* @param ResourceLoaderContext $context Context in which to generate a response
+ * @param string $etag ETag header value
* @return bool If this found a cache file and handled the response
*/
protected function tryRespondFromFileCache(
- ResourceFileCache $fileCache, ResourceLoaderContext $context
+ ResourceFileCache $fileCache,
+ ResourceLoaderContext $context,
+ $etag
) {
$rlMaxage = $this->config->get( 'ResourceLoaderMaxage' );
// Buffer output to catch warnings.
@@ -799,16 +884,12 @@ class ResourceLoader {
if ( $good ) {
$ts = $fileCache->cacheTimestamp();
// Send content type and cache headers
- $this->sendResponseHeaders( $context, $ts, false );
- // If there's an If-Modified-Since header, respond with a 304 appropriately
- if ( $this->tryRespondLastModified( $context, $ts ) ) {
- return false; // output handled (buffers cleared)
- }
+ $this->sendResponseHeaders( $context, $etag, false );
$response = $fileCache->fetchText();
// Capture any PHP warnings from the output buffer and append them to the
// response in a comment if we're in debug mode.
if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
- $response = "/*\n$warnings\n*/\n" . $response;
+ $response = self::makeComment( $warnings ) . $response;
}
// Remove the output buffer and output the response
ob_end_clean();
@@ -854,11 +935,11 @@ class ResourceLoader {
protected static function formatExceptionNoComment( $e ) {
global $wgShowExceptionDetails;
- if ( $wgShowExceptionDetails ) {
- return $e->__toString();
- } else {
- return wfMessage( 'internalerror' )->text();
+ if ( !$wgShowExceptionDetails ) {
+ return 'Internal error';
}
+
+ return $e->__toString();
}
/**
@@ -896,17 +977,14 @@ MESSAGE;
// Pre-fetch blobs
if ( $context->shouldIncludeMessages() ) {
try {
- $blobs = $this->blobStore->get( $this, $modules, $context->getLanguage() );
+ $this->blobStore->get( $this, $modules, $context->getLanguage() );
} catch ( Exception $e ) {
MWExceptionHandler::logException( $e );
- wfDebugLog(
- 'resourceloader',
- __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e"
- );
+ $this->logger->warning( 'Prefetching MessageBlobStore failed: {exception}', array(
+ 'exception' => $e
+ ) );
$this->errors[] = self::formatExceptionNoComment( $e );
}
- } else {
- $blobs = array();
}
foreach ( $missing as $name ) {
@@ -916,116 +994,43 @@ MESSAGE;
// Generate output
$isRaw = false;
foreach ( $modules as $name => $module ) {
- /**
- * @var $module ResourceLoaderModule
- */
-
try {
- $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() && $module->supportsURLLoading() ) {
- $scripts = $module->getScriptURLsForDebug( $context );
- } else {
- $scripts = $module->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";
- }
- }
- }
- // Styles
- $styles = array();
- if ( $context->shouldIncludeStyles() ) {
- // Don't create empty stylesheets like array( '' => '' ) for modules
- // that don't *have* any stylesheets (bug 38024).
- $stylePairs = $module->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() && $module->supportsURLLoading() ) {
- $styles = array(
- 'url' => $module->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][] = $this->filter( 'minify-css', $cssText );
- }
- }
- } elseif ( is_string( $style ) ) {
- $stylePairs[$media] = $this->filter( 'minify-css', $style );
- }
- }
- }
- // Wrap styles into @media groups as needed and flatten into a numerical array
- $styles = array(
- 'css' => self::makeCombinedStyles( $stylePairs )
- );
- }
- }
- }
-
- // Messages
- $messagesBlob = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
+ $content = $module->getModuleContent( $context );
// Append output
switch ( $context->getOnly() ) {
case 'scripts':
+ $scripts = $content['scripts'];
if ( is_string( $scripts ) ) {
// Load scripts raw...
$out .= $scripts;
} elseif ( is_array( $scripts ) ) {
// ...except when $scripts is an array of URLs
- $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() );
+ $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array(), array() );
}
break;
case 'styles':
+ $styles = $content['styles'];
// We no longer seperate into media, they are all combined now with
// custom media type groups into @media .. {} sections as part of the css string.
// Module returns either an empty array or a numerical array with css strings.
$out .= isset( $styles['css'] ) ? implode( '', $styles['css'] ) : '';
break;
- case 'messages':
- $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) );
- break;
- case 'templates':
- $out .= Xml::encodeJsCall(
- 'mw.templates.set',
- array( $name, (object)$module->getTemplates() ),
- ResourceLoader::inDebugMode()
- );
- break;
default:
$out .= self::makeLoaderImplementScript(
$name,
- $scripts,
- $styles,
- new XmlJsCode( $messagesBlob ),
- $module->getTemplates()
+ isset( $content['scripts'] ) ? $content['scripts'] : '',
+ isset( $content['styles'] ) ? $content['styles'] : array(),
+ isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : array(),
+ isset( $content['templates'] ) ? $content['templates'] : array()
);
break;
}
} catch ( Exception $e ) {
MWExceptionHandler::logException( $e );
- wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" );
+ $this->logger->warning( 'Generating module package failed: {exception}', array(
+ 'exception' => $e
+ ) );
$this->errors[] = self::formatExceptionNoComment( $e );
// Respond to client with error-state instead of module implementation
@@ -1056,11 +1061,19 @@ MESSAGE;
}
}
+ $enableFilterCache = true;
+ if ( count( $modules ) === 1 && reset( $modules ) instanceof ResourceLoaderUserTokensModule ) {
+ // If we're building the embedded user.tokens, don't cache (T84960)
+ $enableFilterCache = false;
+ }
+
if ( !$context->getDebug() ) {
if ( $context->getOnly() === 'styles' ) {
$out = $this->filter( 'minify-css', $out );
} else {
- $out = $this->filter( 'minify-js', $out );
+ $out = $this->filter( 'minify-js', $out, array(
+ 'cache' => $enableFilterCache
+ ) );
}
}
@@ -1084,11 +1097,22 @@ MESSAGE;
* @throws MWException
* @return string
*/
- public static function makeLoaderImplementScript( $name, $scripts, $styles,
- $messages, $templates
+ public static function makeLoaderImplementScript(
+ $name, $scripts, $styles, $messages, $templates
) {
if ( is_string( $scripts ) ) {
- $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" );
+ // Site and user module are a legacy scripts that run in the global scope (no closure).
+ // Transportation as string instructs mw.loader.implement to use globalEval.
+ if ( $name === 'site' || $name === 'user' ) {
+ // Minify manually because the general makeModuleResponse() minification won't be
+ // effective here due to the script being a string instead of a function. (T107377)
+ if ( !ResourceLoader::inDebugMode() ) {
+ $scripts = self::applyFilter( 'minify-js', $scripts,
+ ConfigFactory::getDefaultInstance()->makeConfig( 'main' ) );
+ }
+ } else {
+ $scripts = new XmlJsCode( "function ( $, jQuery ) {\n{$scripts}\n}" );
+ }
} elseif ( !is_array( $scripts ) ) {
throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' );
}
@@ -1098,9 +1122,9 @@ MESSAGE;
$module = array(
$name,
$scripts,
- (object) $styles,
- (object) $messages,
- (object) $templates,
+ (object)$styles,
+ (object)$messages,
+ (object)$templates,
);
self::trimArray( $module );
@@ -1193,7 +1217,7 @@ MESSAGE;
* and $group as supplied.
*
* @param string $name Module name
- * @param int $version Module version number as a timestamp
+ * @param string $version Module version hash
* @param array $dependencies List of module names on which this module depends
* @param string $group Group which the module is in.
* @param string $source Source of the module, or 'local' if not foreign.
@@ -1265,7 +1289,7 @@ MESSAGE;
* Registers modules with the given names and parameters.
*
* @param string $name Module name
- * @param int $version Module version number as a timestamp
+ * @param string $version Module version hash
* @param array $dependencies List of module names on which this module depends
* @param string $group Group which the module is in
* @param string $source Source of the module, or 'local' if not foreign
@@ -1346,11 +1370,30 @@ MESSAGE;
* Returns JS code which runs given JS code if the client-side framework is
* present.
*
+ * @deprecated since 1.25; use makeInlineScript instead
* @param string $script JavaScript code
* @return string
*/
public static function makeLoaderConditionalScript( $script ) {
- return "if(window.mw){\n" . trim( $script ) . "\n}";
+ return "window.RLQ = window.RLQ || []; window.RLQ.push( function () {\n" . trim( $script ) . "\n} );";
+ }
+
+ /**
+ * Construct an inline script tag with given JS code.
+ *
+ * The code will be wrapped in a closure, and it will be executed by ResourceLoader
+ * only if the client has adequate support for MediaWiki JavaScript code.
+ *
+ * @param string $script JavaScript code
+ * @return WrappedString HTML
+ */
+ public static function makeInlineScript( $script ) {
+ $js = self::makeLoaderConditionalScript( $script );
+ return new WrappedString(
+ Html::inlineScript( $js ),
+ "<script>window.RLQ = window.RLQ || []; window.RLQ.push( function () {\n",
+ "\n} );</script>"
+ );
}
/**
@@ -1361,11 +1404,13 @@ MESSAGE;
* @return string
*/
public static function makeConfigSetScript( array $configuration ) {
- return Xml::encodeJsCall(
- 'mw.config.set',
- array( $configuration ),
- ResourceLoader::inDebugMode()
- );
+ if ( ResourceLoader::inDebugMode() ) {
+ return Xml::encodeJsCall( 'mw.config.set', array( $configuration ), true );
+ }
+
+ $config = RequestContext::getMain()->getConfig();
+ $js = Xml::encodeJsCall( 'mw.config.set', array( $configuration ), false );
+ return self::applyFilter( 'minify-js', $js, $config );
}
/**
@@ -1427,7 +1472,7 @@ MESSAGE;
* @param string $source Name of the ResourceLoader source
* @param ResourceLoaderContext $context
* @param array $extraQuery
- * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative)
+ * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too.
*/
public function createLoaderURL( $source, ResourceLoaderContext $context,
$extraQuery = array()
@@ -1435,14 +1480,12 @@ MESSAGE;
$query = self::createLoaderQuery( $context, $extraQuery );
$script = $this->getLoadScript( $source );
- // Prevent the IE6 extension check from being triggered (bug 28840)
- // by appending a character that's invalid in Windows extensions ('*')
- return wfExpandUrl( wfAppendQuery( $script, $query ) . '&*', PROTO_RELATIVE );
+ return wfAppendQuery( $script, $query );
}
/**
* Build a load.php URL
- * @deprecated since 1.24, use createLoaderURL instead
+ * @deprecated since 1.24 Use createLoaderURL() instead
* @param array $modules Array of module names (strings)
* @param string $lang Language code
* @param string $skin Skin name
@@ -1453,7 +1496,7 @@ MESSAGE;
* @param bool $printable Printable mode
* @param bool $handheld Handheld mode
* @param array $extraQuery Extra query parameters to add
- * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative)
+ * @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too.
*/
public static function makeLoaderURL( $modules, $lang, $skin, $user = null,
$version = null, $debug = false, $only = null, $printable = false,
@@ -1465,9 +1508,7 @@ MESSAGE;
$only, $printable, $handheld, $extraQuery
);
- // Prevent the IE6 extension check from being triggered (bug 28840)
- // by appending a character that's invalid in Windows extensions ('*')
- return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE );
+ return wfAppendQuery( $wgLoadScript, $query );
}
/**
@@ -1562,27 +1603,23 @@ MESSAGE;
* @param Config $config
* @throws MWException
* @since 1.22
- * @return lessc
+ * @return Less_Parser
*/
public static function getLessCompiler( Config $config ) {
// When called from the installer, it is possible that a required PHP extension
// is missing (at least for now; see bug 47564). If this is the case, throw an
// exception (caught by the installer) to prevent a fatal error later on.
- if ( !class_exists( 'lessc' ) ) {
- throw new MWException( 'MediaWiki requires the lessphp compiler' );
- }
- if ( !function_exists( 'ctype_digit' ) ) {
- throw new MWException( 'lessc requires the Ctype extension' );
+ if ( !class_exists( 'Less_Parser' ) ) {
+ throw new MWException( 'MediaWiki requires the less.php parser' );
}
- $less = new lessc();
- $less->setPreserveComments( true );
- $less->setVariables( self::getLessVars( $config ) );
- $less->setImportDir( $config->get( 'ResourceLoaderLESSImportPaths' ) );
- foreach ( $config->get( 'ResourceLoaderLESSFunctions' ) as $name => $func ) {
- $less->registerFunction( $name, $func );
- }
- return $less;
+ $parser = new Less_Parser;
+ $parser->ModifyVars( self::getLessVars( $config ) );
+ $parser->SetImportDirs( array_fill_keys( $config->get( 'ResourceLoaderLESSImportPaths' ), '' ) );
+ $parser->SetOption( 'relativeUrls', false );
+ $parser->SetCacheDir( $config->get( 'CacheDirectory' ) ?: wfTempDir() );
+
+ return $parser;
}
/**
diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php
index a6a7d347..2e1752a6 100644
--- a/includes/resourceloader/ResourceLoaderContext.php
+++ b/includes/resourceloader/ResourceLoaderContext.php
@@ -22,6 +22,8 @@
* @author Roan Kattouw
*/
+use MediaWiki\Logger\LoggerFactory;
+
/**
* Object passed around to modules which contains information about the state
* of a specific loader request
@@ -57,24 +59,26 @@ class ResourceLoaderContext {
$this->resourceLoader = $resourceLoader;
$this->request = $request;
- // Interpret request
// List of modules
$modules = $request->getVal( 'modules' );
$this->modules = $modules ? self::expandModuleNames( $modules ) : array();
+
// Various parameters
- $this->skin = $request->getVal( 'skin' );
$this->user = $request->getVal( 'user' );
$this->debug = $request->getFuzzyBool(
- 'debug', $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' )
+ 'debug',
+ $resourceLoader->getConfig()->get( 'ResourceLoaderDebug' )
);
- $this->only = $request->getVal( 'only' );
- $this->version = $request->getVal( 'version' );
+ $this->only = $request->getVal( 'only', null );
+ $this->version = $request->getVal( 'version', null );
$this->raw = $request->getFuzzyBool( 'raw' );
+
// Image requests
$this->image = $request->getVal( 'image' );
$this->variant = $request->getVal( 'variant' );
$this->format = $request->getVal( 'format' );
+ $this->skin = $request->getVal( 'skin' );
$skinnames = Skin::getSkinNames();
// If no skin is specified, or we don't recognize the skin, use the default skin
if ( !$this->skin || !isset( $skinnames[$this->skin] ) ) {
@@ -123,7 +127,8 @@ class ResourceLoaderContext {
*/
public static function newDummyContext() {
return new self( new ResourceLoader(
- ConfigFactory::getDefaultInstance()->makeConfig( 'main' )
+ ConfigFactory::getDefaultInstance()->makeConfig( 'main' ),
+ LoggerFactory::getInstance( 'resourceloader' )
), new FauxRequest( array() ) );
}
@@ -154,7 +159,7 @@ class ResourceLoaderContext {
public function getLanguage() {
if ( $this->language === null ) {
// Must be a valid language code after this point (bug 62849)
- $this->language = RequestContext::sanitizeLangCode( $this->request->getVal( 'lang' ) );
+ $this->language = RequestContext::sanitizeLangCode( $this->getRequest()->getVal( 'lang' ) );
}
return $this->language;
}
@@ -164,7 +169,7 @@ class ResourceLoaderContext {
*/
public function getDirection() {
if ( $this->direction === null ) {
- $this->direction = $this->request->getVal( 'dir' );
+ $this->direction = $this->getRequest()->getVal( 'dir' );
if ( !$this->direction ) {
// Determine directionality based on user language (bug 6100)
$this->direction = Language::factory( $this->getLanguage() )->getDir();
@@ -174,7 +179,7 @@ class ResourceLoaderContext {
}
/**
- * @return string|null
+ * @return string
*/
public function getSkin() {
return $this->skin;
@@ -227,6 +232,8 @@ class ResourceLoaderContext {
}
/**
+ * @see ResourceLoaderModule::getVersionHash
+ * @see OutputPage::makeResourceLoaderLink
* @return string|null
*/
public function getVersion() {
@@ -285,7 +292,7 @@ class ResourceLoaderContext {
return $this->imageObj;
}
- $image = $module->getImage( $this->image );
+ $image = $module->getImage( $this->image, $this );
if ( !$image ) {
return $this->imageObj;
}
@@ -300,21 +307,21 @@ class ResourceLoaderContext {
* @return bool
*/
public function shouldIncludeScripts() {
- return is_null( $this->getOnly() ) || $this->getOnly() === 'scripts';
+ return $this->getOnly() === null || $this->getOnly() === 'scripts';
}
/**
* @return bool
*/
public function shouldIncludeStyles() {
- return is_null( $this->getOnly() ) || $this->getOnly() === 'styles';
+ return $this->getOnly() === null || $this->getOnly() === 'styles';
}
/**
* @return bool
*/
public function shouldIncludeMessages() {
- return is_null( $this->getOnly() ) || $this->getOnly() === 'messages';
+ return $this->getOnly() === null;
}
/**
diff --git a/includes/resourceloader/ResourceLoaderEditToolbarModule.php b/includes/resourceloader/ResourceLoaderEditToolbarModule.php
index d79174cd..da729fdc 100644
--- a/includes/resourceloader/ResourceLoaderEditToolbarModule.php
+++ b/includes/resourceloader/ResourceLoaderEditToolbarModule.php
@@ -56,7 +56,7 @@ class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule {
// This is very conveniently formatted and we can pass it right through
$vars = $language->getImageFiles();
- // lessc tries to be helpful and parse our variables as LESS source code
+ // less.php tries to be helpful and parse our variables as LESS source code
foreach ( $vars as $key => &$value ) {
$value = self::cssSerializeString( $value );
}
@@ -65,25 +65,10 @@ class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule {
}
/**
- * @param ResourceLoaderContext $context
- * @return int UNIX timestamp
- */
- public function getModifiedTime( ResourceLoaderContext $context ) {
- return max(
- parent::getModifiedTime( $context ),
- $this->getHashMtime( $context )
- );
- }
-
- /**
- * @param ResourceLoaderContext $context
- * @return string Hash
+ * @return bool
*/
- public function getModifiedHash( ResourceLoaderContext $context ) {
- return md5(
- parent::getModifiedHash( $context ) .
- serialize( $this->getLessVars( $context ) )
- );
+ public function enableModuleContentVersion() {
+ return true;
}
/**
@@ -93,11 +78,11 @@ class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule {
*
* @throws MWException
* @param ResourceLoaderContext $context
- * @return lessc
+ * @return Less_Parser
*/
protected function getLessCompiler( ResourceLoaderContext $context = null ) {
- $compiler = parent::getLessCompiler();
- $compiler->setVariables( $this->getLessVars( $context ) );
- return $compiler;
+ $parser = parent::getLessCompiler();
+ $parser->ModifyVars( $this->getLessVars( $context ) );
+ return $parser;
}
}
diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php
index 671098e1..7fbc1cb4 100644
--- a/includes/resourceloader/ResourceLoaderFileModule.php
+++ b/includes/resourceloader/ResourceLoaderFileModule.php
@@ -144,15 +144,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
protected $hasGeneratedStyles = false;
/**
- * @var array Cache for mtime
- * @par Usage:
- * @code
- * array( [hash] => [mtime], [hash] => [mtime], ... )
- * @endcode
- */
- protected $modifiedTime = array();
-
- /**
* @var array Place where readStyleFile() tracks file dependencies
* @par Usage:
* @code
@@ -161,6 +152,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
*/
protected $localFileRefs = array();
+ /**
+ * @var array Place where readStyleFile() tracks file dependencies for non-existent files.
+ * Used in tests to detect missing dependencies.
+ */
+ protected $missingLocalFileRefs = array();
+
/* Methods */
/**
@@ -281,8 +278,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
$this->{$member} = $option;
break;
// Single strings
- case 'group':
case 'position':
+ $this->isPositionDefined = true;
+ case 'group':
case 'skipFunction':
$this->{$member} = (string)$option;
break;
@@ -486,10 +484,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
/**
* Gets list of names of modules this module depends on.
- *
+ * @param ResourceLoaderContext context
* @return array List of module names
*/
- public function getDependencies() {
+ public function getDependencies( ResourceLoaderContext $context = null ) {
return $this->dependencies;
}
@@ -522,24 +520,28 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
}
/**
- * Get the last modified timestamp of this module.
+ * Disable module content versioning.
*
- * Last modified timestamps are calculated from the highest last modified
- * timestamp of this module's constituent files as well as the files it
- * depends on. This function is context-sensitive, only performing
- * calculations on files relevant to the given language, skin and debug
- * mode.
+ * This class uses getDefinitionSummary() instead, to avoid filesystem overhead
+ * involved with building the full module content inside a startup request.
*
- * @param ResourceLoaderContext $context Context in which to calculate
- * the modified time
- * @return int UNIX timestamp
- * @see ResourceLoaderModule::getFileDependencies
+ * @return bool
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
- if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
- return $this->modifiedTime[$context->getHash()];
- }
+ public function enableModuleContentVersion() {
+ return false;
+ }
+ /**
+ * Helper method to gather file hashes for getDefinitionSummary.
+ *
+ * This function is context-sensitive, only computing hashes of files relevant to the
+ * given language, skin, etc.
+ *
+ * @see ResourceLoaderModule::getFileDependencies
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ protected function getFileHashes( ResourceLoaderContext $context ) {
$files = array();
// Flatten style files into $files
@@ -578,22 +580,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
// entry point Less file we already know about.
$files = array_values( array_unique( $files ) );
- // If a module is nothing but a list of dependencies, we need to avoid
- // giving max() an empty array
- if ( count( $files ) === 0 ) {
- $this->modifiedTime[$context->getHash()] = 1;
- return $this->modifiedTime[$context->getHash()];
- }
-
- $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) );
-
- $this->modifiedTime[$context->getHash()] = max(
- $filesMtime,
- $this->getMsgBlobMtime( $context->getLanguage() ),
- $this->getDefinitionMtime( $context )
- );
-
- return $this->modifiedTime[$context->getHash()];
+ // Don't include keys or file paths here, only the hashes. Including that would needlessly
+ // cause global cache invalidation when files move or if e.g. the MediaWiki path changes.
+ // Any significant ordering is already detected by the definition summary.
+ return array_map( array( __CLASS__, 'safeFileHash' ), $files );
}
/**
@@ -604,7 +594,17 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
*/
public function getDefinitionSummary( ResourceLoaderContext $context ) {
$summary = parent::getDefinitionSummary( $context );
+
+ $options = array();
foreach ( array(
+ // The following properties are omitted because they don't affect the module reponse:
+ // - localBasePath (Per T104950; Changes when absolute directory name changes. If
+ // this affects 'scripts' and other file paths, getFileHashes accounts for that.)
+ // - remoteBasePath (Per T104950)
+ // - dependencies (provided via startup module)
+ // - targets
+ // - group (provided via startup module)
+ // - position (only used by OutputPage)
'scripts',
'debugScripts',
'loaderScripts',
@@ -612,25 +612,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
'languageScripts',
'skinScripts',
'skinStyles',
- 'dependencies',
'messages',
- 'targets',
'templates',
- 'group',
- 'position',
'skipFunction',
- 'localBasePath',
- 'remoteBasePath',
'debugRaw',
'raw',
) as $member ) {
- $summary[$member] = $this->{$member};
+ $options[$member] = $this->{$member};
};
+
+ $summary[] = array(
+ 'options' => $options,
+ 'fileHashes' => $this->getFileHashes( $context ),
+ 'msgBlobMtime' => $this->getMsgBlobMtime( $context->getLanguage() ),
+ );
return $summary;
}
- /* Protected Methods */
-
/**
* @param string|ResourceLoaderFilePath $path
* @return string
@@ -925,10 +923,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
$localDir = dirname( $localPath );
$remoteDir = dirname( $remotePath );
// Get and register local file references
- $this->localFileRefs = array_merge(
- $this->localFileRefs,
- CSSMin::getLocalFileReferences( $style, $localDir )
- );
+ $localFileRefs = CSSMin::getAllLocalFileReferences( $style, $localDir );
+ foreach ( $localFileRefs as $file ) {
+ if ( file_exists( $file ) ) {
+ $this->localFileRefs[] = $file;
+ } else {
+ $this->missingLocalFileRefs[] = $file;
+ }
+ }
return CSSMin::remap(
$style, $localDir, $remoteDir, true
);
@@ -958,17 +960,17 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
* Keeps track of all used files and adds them to localFileRefs.
*
* @since 1.22
- * @throws Exception If lessc encounters a parse error
+ * @throws Exception If less.php encounters a parse error
* @param string $fileName File path of LESS source
- * @param lessc $compiler Compiler to use, if not default
+ * @param Less_Parser $parser Compiler to use, if not default
* @return string CSS source
*/
protected function compileLessFile( $fileName, $compiler = null ) {
if ( !$compiler ) {
$compiler = $this->getLessCompiler();
}
- $result = $compiler->compileFile( $fileName );
- $this->localFileRefs += array_keys( $compiler->allParsedFiles() );
+ $result = $compiler->parseFile( $fileName )->getCss();
+ $this->localFileRefs += array_keys( $compiler->AllParsedFiles() );
return $result;
}
@@ -980,7 +982,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule {
* @param ResourceLoaderContext $context
* @throws MWException
* @since 1.24
- * @return lessc
+ * @return Less_Parser
*/
protected function getLessCompiler( ResourceLoaderContext $context = null ) {
return ResourceLoader::getLessCompiler( $this->getConfig() );
diff --git a/includes/resourceloader/ResourceLoaderForeignApiModule.php b/includes/resourceloader/ResourceLoaderForeignApiModule.php
new file mode 100644
index 00000000..7ed08317
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderForeignApiModule.php
@@ -0,0 +1,33 @@
+<?php
+/**
+ * ResourceLoader module for mediawiki.ForeignApi that has dynamically
+ * generated dependencies, via a hook usable by extensions.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * ResourceLoader module for mediawiki.ForeignApi and its generated data
+ */
+class ResourceLoaderForeignApiModule extends ResourceLoaderFileModule {
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ $dependencies = $this->dependencies;
+ Hooks::run( 'ResourceLoaderForeignApiModules', array( &$dependencies, $context ) );
+ return $dependencies;
+ }
+}
diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php
index 12d1e827..2338c902 100644
--- a/includes/resourceloader/ResourceLoaderImage.php
+++ b/includes/resourceloader/ResourceLoaderImage.php
@@ -54,15 +54,16 @@ class ResourceLoaderImage {
$this->variants = $variants;
// Expand shorthands:
- // array( "en,de,fr" => "foo.svg" ) → array( "en" => "foo.svg", "de" => "foo.svg", "fr" => "foo.svg" )
+ // array( "en,de,fr" => "foo.svg" )
+ // → array( "en" => "foo.svg", "de" => "foo.svg", "fr" => "foo.svg" )
if ( is_array( $this->descriptor ) && isset( $this->descriptor['lang'] ) ) {
foreach ( array_keys( $this->descriptor['lang'] ) as $langList ) {
if ( strpos( $langList, ',' ) !== false ) {
$this->descriptor['lang'] += array_fill_keys(
explode( ',', $langList ),
- $this->descriptor['lang'][ $langList ]
+ $this->descriptor['lang'][$langList]
);
- unset( $this->descriptor['lang'][ $langList ] );
+ unset( $this->descriptor['lang'][$langList] );
}
}
}
@@ -75,11 +76,15 @@ class ResourceLoaderImage {
} );
$extensions = array_unique( $extensions );
if ( count( $extensions ) !== 1 ) {
- throw new InvalidArgumentException( "File type for different image files of '$name' not the same" );
+ throw new InvalidArgumentException(
+ "File type for different image files of '$name' not the same"
+ );
}
$ext = $extensions[0];
if ( !isset( self::$fileTypes[$ext] ) ) {
- throw new InvalidArgumentException( "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg)" );
+ throw new InvalidArgumentException(
+ "Invalid file type for image files of '$name' (valid: svg, png, gif, jpg)"
+ );
}
$this->extension = $ext;
}
@@ -117,14 +122,14 @@ class ResourceLoaderImage {
* @param ResourceLoaderContext $context Any context
* @return string
*/
- protected function getPath( ResourceLoaderContext $context ) {
+ public function getPath( ResourceLoaderContext $context ) {
$desc = $this->descriptor;
if ( is_string( $desc ) ) {
return $this->basePath . '/' . $desc;
- } elseif ( isset( $desc['lang'][ $context->getLanguage() ] ) ) {
- return $this->basePath . '/' . $desc['lang'][ $context->getLanguage() ];
- } elseif ( isset( $desc[ $context->getDirection() ] ) ) {
- return $this->basePath . '/' . $desc[ $context->getDirection() ];
+ } elseif ( isset( $desc['lang'][$context->getLanguage()] ) ) {
+ return $this->basePath . '/' . $desc['lang'][$context->getLanguage()];
+ } elseif ( isset( $desc[$context->getDirection()] ) ) {
+ return $this->basePath . '/' . $desc[$context->getDirection()];
} else {
return $this->basePath . '/' . $desc['default'];
}
diff --git a/includes/resourceloader/ResourceLoaderImageModule.php b/includes/resourceloader/ResourceLoaderImageModule.php
index bf6a7dd2..8de87f2e 100644
--- a/includes/resourceloader/ResourceLoaderImageModule.php
+++ b/includes/resourceloader/ResourceLoaderImageModule.php
@@ -28,6 +28,8 @@
*/
class ResourceLoaderImageModule extends ResourceLoaderModule {
+ protected $definition = null;
+
/**
* Local base path, see __construct()
* @var string
@@ -43,6 +45,9 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
protected $selectorWithVariant = '.{prefix}-{name}-{variant}';
protected $targets = array( 'desktop', 'mobile' );
+ /** @var string Position on the page to load this module at */
+ protected $position = 'bottom';
+
/**
* Constructs a new module from an options array.
*
@@ -57,6 +62,8 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
* array(
* // Base path to prepend to all local paths in $options. Defaults to $IP
* 'localBasePath' => [base path],
+ * // Path to JSON file that contains any of the settings below
+ * 'data' => [file path string]
* // CSS class prefix to use in all style rules
* 'prefix' => [CSS class prefix],
* // Alternatively: Format of CSS selector to use in all style rules
@@ -66,21 +73,29 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
* 'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}],
* // List of variants that may be used for the image files
* 'variants' => array(
+ * [theme name] => array(
* [variant name] => array(
* 'color' => [color string, e.g. '#ffff00'],
* 'global' => [boolean, if true, this variant is available
* for all images of this type],
* ),
+ * ...
+ * ),
* ...
* ),
* // List of image files and their options
* 'images' => array(
- * [file path string],
- * [file path string] => array(
- * 'name' => [image name string, defaults to file name],
+ * [theme name] => array(
+ * [icon name] => array(
+ * 'file' => [file path string or array whose values are file path strings
+ * and whose keys are 'default', 'ltr', 'rtl', a single
+ * language code like 'en', or a list of language codes like
+ * 'en,de,ar'],
* 'variants' => [array of variant name strings, variants
* available for this image],
* ),
+ * ...
+ * ),
* ...
* ),
* )
@@ -90,6 +105,26 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
public function __construct( $options = array(), $localBasePath = null ) {
$this->localBasePath = self::extractLocalBasePath( $options, $localBasePath );
+ $this->definition = $options;
+ }
+
+ /**
+ * Parse definition and external JSON data, if referenced.
+ */
+ protected function loadFromDefinition() {
+ if ( $this->definition === null ) {
+ return;
+ }
+
+ $options = $this->definition;
+ $this->definition = null;
+
+ if ( isset( $options['data'] ) ) {
+ $dataPath = $this->localBasePath . '/' . $options['data'];
+ $data = json_decode( file_get_contents( $dataPath ), true );
+ $options = array_merge( $data, $options );
+ }
+
// Accepted combinations:
// * prefix
// * selector
@@ -99,20 +134,30 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
$prefix = isset( $options['prefix'] ) && $options['prefix'];
$selector = isset( $options['selector'] ) && $options['selector'];
- $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] ) && $options['selectorWithoutVariant'];
- $selectorWithVariant = isset( $options['selectorWithVariant'] ) && $options['selectorWithVariant'];
+ $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] )
+ && $options['selectorWithoutVariant'];
+ $selectorWithVariant = isset( $options['selectorWithVariant'] )
+ && $options['selectorWithVariant'];
if ( $selectorWithoutVariant && !$selectorWithVariant ) {
- throw new InvalidArgumentException( "Given 'selectorWithoutVariant' but no 'selectorWithVariant'." );
+ throw new InvalidArgumentException(
+ "Given 'selectorWithoutVariant' but no 'selectorWithVariant'."
+ );
}
if ( $selectorWithVariant && !$selectorWithoutVariant ) {
- throw new InvalidArgumentException( "Given 'selectorWithVariant' but no 'selectorWithoutVariant'." );
+ throw new InvalidArgumentException(
+ "Given 'selectorWithVariant' but no 'selectorWithoutVariant'."
+ );
}
if ( $selector && $selectorWithVariant ) {
- throw new InvalidArgumentException( "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given." );
+ throw new InvalidArgumentException(
+ "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given."
+ );
}
if ( !$prefix && !$selector && !$selectorWithVariant ) {
- throw new InvalidArgumentException( "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given." );
+ throw new InvalidArgumentException(
+ "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given."
+ );
}
foreach ( $options as $member => $option ) {
@@ -124,9 +169,22 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
"Invalid list error. '$option' given, array expected."
);
}
+ if ( !isset( $option['default'] ) ) {
+ // Backwards compatibility
+ $option = array( 'default' => $option );
+ }
+ foreach ( $option as $skin => $data ) {
+ if ( !is_array( $option ) ) {
+ throw new InvalidArgumentException(
+ "Invalid list error. '$option' given, array expected."
+ );
+ }
+ }
$this->{$member} = $option;
break;
+ case 'position':
+ $this->isPositionDefined = true;
case 'prefix':
case 'selectorWithoutVariant':
case 'selectorWithVariant':
@@ -144,6 +202,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
* @return string
*/
public function getPrefix() {
+ $this->loadFromDefinition();
return $this->prefix;
}
@@ -152,6 +211,7 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
* @return string
*/
public function getSelectors() {
+ $this->loadFromDefinition();
return array(
'selectorWithoutVariant' => $this->selectorWithoutVariant,
'selectorWithVariant' => $this->selectorWithVariant,
@@ -161,31 +221,43 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
/**
* Get a ResourceLoaderImage object for given image.
* @param string $name Image name
+ * @param ResourceLoaderContext $context
* @return ResourceLoaderImage|null
*/
- public function getImage( $name ) {
- $images = $this->getImages();
+ public function getImage( $name, ResourceLoaderContext $context ) {
+ $this->loadFromDefinition();
+ $images = $this->getImages( $context );
return isset( $images[$name] ) ? $images[$name] : null;
}
/**
* Get ResourceLoaderImage objects for all images.
+ * @param ResourceLoaderContext $context
* @return ResourceLoaderImage[] Array keyed by image name
*/
- public function getImages() {
+ public function getImages( ResourceLoaderContext $context ) {
+ $skin = $context->getSkin();
if ( !isset( $this->imageObjects ) ) {
+ $this->loadFromDefinition();
$this->imageObjects = array();
-
- foreach ( $this->images as $name => $options ) {
+ }
+ if ( !isset( $this->imageObjects[$skin] ) ) {
+ $this->imageObjects[$skin] = array();
+ if ( !isset( $this->images[$skin] ) ) {
+ $this->images[$skin] = isset( $this->images['default'] ) ?
+ $this->images['default'] :
+ array();
+ }
+ foreach ( $this->images[$skin] as $name => $options ) {
$fileDescriptor = is_string( $options ) ? $options : $options['file'];
$allowedVariants = array_merge(
is_array( $options ) && isset( $options['variants'] ) ? $options['variants'] : array(),
- $this->getGlobalVariants()
+ $this->getGlobalVariants( $context )
);
- if ( isset( $this->variants ) ) {
+ if ( isset( $this->variants[$skin] ) ) {
$variantConfig = array_intersect_key(
- $this->variants,
+ $this->variants[$skin],
array_fill_keys( $allowedVariants, true )
);
} else {
@@ -199,32 +271,40 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
$this->localBasePath,
$variantConfig
);
- $this->imageObjects[ $image->getName() ] = $image;
+ $this->imageObjects[$skin][$image->getName()] = $image;
}
}
- return $this->imageObjects;
+ return $this->imageObjects[$skin];
}
/**
* Get list of variants in this module that are 'global', i.e., available
* for every image regardless of image options.
+ * @param ResourceLoaderContext $context
* @return string[]
*/
- public function getGlobalVariants() {
+ public function getGlobalVariants( ResourceLoaderContext $context ) {
+ $skin = $context->getSkin();
if ( !isset( $this->globalVariants ) ) {
+ $this->loadFromDefinition();
$this->globalVariants = array();
-
- if ( isset( $this->variants ) ) {
- foreach ( $this->variants as $name => $config ) {
- if ( isset( $config['global'] ) && $config['global'] ) {
- $this->globalVariants[] = $name;
- }
+ }
+ if ( !isset( $this->globalVariants[$skin] ) ) {
+ $this->globalVariants[$skin] = array();
+ if ( !isset( $this->variants[$skin] ) ) {
+ $this->variants[$skin] = isset( $this->variants['default'] ) ?
+ $this->variants['default'] :
+ array();
+ }
+ foreach ( $this->variants[$skin] as $name => $config ) {
+ if ( isset( $config['global'] ) && $config['global'] ) {
+ $this->globalVariants[$skin][] = $name;
}
}
}
- return $this->globalVariants;
+ return $this->globalVariants[$skin];
}
/**
@@ -232,12 +312,14 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
* @return array
*/
public function getStyles( ResourceLoaderContext $context ) {
+ $this->loadFromDefinition();
+
// Build CSS rules
$rules = array();
$script = $context->getResourceLoader()->getLoadScript( $this->getSource() );
$selectors = $this->getSelectors();
- foreach ( $this->getImages() as $name => $image ) {
+ foreach ( $this->getImages( $context ) as $name => $image ) {
$declarations = $this->getCssDeclarations(
$image->getDataUri( $context, null, 'original' ),
$image->getUrl( $context, $script, null, 'rasterized' )
@@ -304,6 +386,48 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
}
/**
+ * Get the definition summary for this module.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getDefinitionSummary( ResourceLoaderContext $context ) {
+ $this->loadFromDefinition();
+ $summary = parent::getDefinitionSummary( $context );
+ foreach ( array(
+ 'localBasePath',
+ 'images',
+ 'variants',
+ 'prefix',
+ 'selectorWithoutVariant',
+ 'selectorWithVariant',
+ ) as $member ) {
+ $summary[$member] = $this->{$member};
+ };
+ return $summary;
+ }
+
+ /**
+ * Get the last modified timestamp of this module.
+ *
+ * @param ResourceLoaderContext $context Context in which to calculate
+ * the modified time
+ * @return int UNIX timestamp
+ */
+ public function getModifiedTime( ResourceLoaderContext $context ) {
+ $this->loadFromDefinition();
+ $files = array();
+ foreach ( $this->getImages( $context ) as $name => $image ) {
+ $files[] = $image->getPath( $context );
+ }
+
+ $files = array_values( array_unique( $files ) );
+ $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) );
+
+ return $filesMtime;
+ }
+
+ /**
* Extract a local base path from module definition information.
*
* @param array $options Module definition
@@ -324,4 +448,17 @@ class ResourceLoaderImageModule extends ResourceLoaderModule {
return $localBasePath;
}
+
+ /**
+ * @return string
+ */
+ public function getPosition() {
+ $this->loadFromDefinition();
+ return $this->position;
+ }
+
+ public function isPositionDefault() {
+ $this->loadFromDefinition();
+ return parent::isPositionDefault();
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderJqueryMsgModule.php b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php
new file mode 100644
index 00000000..f9dfbdc2
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderJqueryMsgModule.php
@@ -0,0 +1,66 @@
+<?php
+/**
+ * ResourceLoader module for mediawiki.jqueryMsg that provides generated data.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Brad Jorsch
+ */
+
+/**
+ * ResourceLoader module for mediawiki.jqueryMsg and its generated data
+ */
+class ResourceLoaderJqueryMsgModule extends ResourceLoaderFileModule {
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ $fileScript = parent::getScript( $context );
+
+ $tagData = Sanitizer::getRecognizedTagData();
+ $parserDefaults = array();
+ $parserDefaults['allowedHtmlElements'] = array_merge(
+ array_keys( $tagData['htmlpairs'] ),
+ array_diff(
+ array_keys( $tagData['htmlsingle'] ),
+ array_keys( $tagData['htmlsingleonly'] )
+ )
+ );
+
+ $dataScript = Xml::encodeJsCall( 'mw.jqueryMsg.setParserDefaults', array( $parserDefaults ) );
+
+ return $fileScript . $dataScript;
+ }
+
+ /**
+ * @param ResourceLoaderContext $context
+ * @return array
+ */
+ public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
+ // Bypass file module urls
+ return ResourceLoaderModule::getScriptURLsForDebug( $context );
+ }
+
+ /**
+ * @return bool
+ */
+ public function enableModuleContentVersion() {
+ return true;
+ }
+}
diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
index 12394536..27c74d74 100644
--- a/includes/resourceloader/ResourceLoaderLanguageDataModule.php
+++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php
@@ -63,25 +63,17 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule {
}
/**
- * @param ResourceLoaderContext $context
- * @return int UNIX timestamp
+ * @return bool
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
- return max( 1, $this->getHashMtime( $context ) );
+ public function enableModuleContentVersion() {
+ return true;
}
/**
* @param ResourceLoaderContext $context
- * @return string Hash
- */
- public function getModifiedHash( ResourceLoaderContext $context ) {
- return md5( serialize( $this->getData( $context ) ) );
- }
-
- /**
* @return array
*/
- public function getDependencies() {
+ public function getDependencies( ResourceLoaderContext $context = null ) {
return array( 'mediawiki.language.init' );
}
}
diff --git a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php
index 55b1f4b1..081c728c 100644
--- a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php
+++ b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php
@@ -32,7 +32,6 @@ class ResourceLoaderLanguageNamesModule extends ResourceLoaderModule {
protected $targets = array( 'desktop', 'mobile' );
-
/**
* @param ResourceLoaderContext $context
* @return array
@@ -60,24 +59,19 @@ class ResourceLoaderLanguageNamesModule extends ResourceLoaderModule {
);
}
- public function getDependencies() {
- return array( 'mediawiki.language.init' );
- }
-
/**
* @param ResourceLoaderContext $context
- * @return int UNIX timestamp
+ * @return array
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
- return max( 1, $this->getHashMtime( $context ) );
+ public function getDependencies( ResourceLoaderContext $context = null ) {
+ return array( 'mediawiki.language.init' );
}
/**
- * @param ResourceLoaderContext $context
- * @return string Hash
+ * @return bool
*/
- public function getModifiedHash( ResourceLoaderContext $context ) {
- return md5( serialize( $this->getData( $context ) ) );
+ public function enableModuleContentVersion() {
+ return true;
}
}
diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php
index ed16521b..1d3ffb55 100644
--- a/includes/resourceloader/ResourceLoaderModule.php
+++ b/includes/resourceloader/ResourceLoaderModule.php
@@ -29,7 +29,6 @@ abstract class ResourceLoaderModule {
# Type of resource
const TYPE_SCRIPTS = 'scripts';
const TYPE_STYLES = 'styles';
- const TYPE_MESSAGES = 'messages';
const TYPE_COMBINED = 'combined';
# sitewide core module like a skin file or jQuery component
@@ -63,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
@@ -285,6 +292,19 @@ abstract class ResourceLoaderModule {
}
/**
+ * 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
* appended to the bottom of the script.
@@ -313,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();
}
@@ -361,16 +386,21 @@ abstract class ResourceLoaderModule {
}
$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];
}
@@ -385,8 +415,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
*/
@@ -397,10 +426,13 @@ abstract class ResourceLoaderModule {
}
$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
@@ -422,144 +454,318 @@ 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.
- *
- * 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.
+ * Get an array of this module's resources. Ready for serving to the web.
*
- * @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();
+ $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()];
}
- // 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;
+ $templates = $this->getTemplates();
+ if ( $templates ) {
+ $content['templates'] = $templates;
}
- $timestamp = time();
- $cache->set( $key, $timestamp );
- return $timestamp;
+ $statTiming = microtime( true ) - $statStart;
+ $statName = strtr( $this->getName(), '.', '_' );
+ $stats->timing( "resourceloader_build.all", 1000 * $statTiming );
+ $stats->timing( "resourceloader_build.$statName", 1000 * $statTiming );
+
+ 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.
+ *
+ * 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.)
*
- * This is the method subclasses should implement if they want to make
- * use of getHashMTime() inside getModifiedTime().
+ * It is not recommended to override this directly. Use getDefinitionSummary() instead.
+ * If overridden, one must call the parent getVersionHash(), append data and re-hash.
*
+ * 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 string|null Hash
- */
- public function getModifiedHash( ResourceLoaderContext $context ) {
- return null;
+ * @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 ) ) {
+
+ 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 );
+ }
+ }
+
+ $this->versionHash[$contextHash] = ResourceLoader::makeHash( $str );
+ }
+ return $this->versionHash[$contextHash];
}
/**
- * Helper method for calculating when this module's definition summary was last changed.
+ * Whether to generate version hash based on module content.
*
- * @since 1.23
+ * 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.
*
- * @param ResourceLoaderContext $context
- * @return int UNIX timestamp
+ * @return bool
*/
- public function getDefinitionMtime( ResourceLoaderContext $context ) {
- $summary = $this->getDefinitionSummary( $context );
- if ( $summary === null ) {
- return 1;
- }
-
- $hash = md5( json_encode( $summary ) );
- $cache = wfGetCache( CACHE_ANYTHING );
- $key = wfMemcKey( 'resourceloader', 'moduledefinition', $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;
- }
-
- wfDebugLog( 'resourceloader', __METHOD__ . ": New definition for module "
- . "{$this->getName()} in context \"{$context->getHash()}\"" );
-
- $timestamp = time();
- $cache->set( $key, $timestamp );
- return $timestamp;
+ public function enableModuleContentVersion() {
+ return false;
}
/**
* 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
* definitely going to be empty, it should override this method to
@@ -587,8 +793,13 @@ abstract class ResourceLoaderModule {
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 ) ) {
@@ -602,7 +813,8 @@ 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 );
@@ -623,16 +835,57 @@ abstract class ResourceLoaderModule {
}
/**
- * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist
- * but returns 1 instead.
- * @param string $filename File name
+ * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist.
+ * Defaults to 1.
+ *
+ * @param string $filePath File path
* @return int UNIX timestamp
*/
- protected static function safeFilemtime( $filename ) {
- wfSuppressWarnings();
- $mtime = filemtime( $filename ) ?: 1;
- wfRestoreWarnings();
-
+ protected static function safeFilemtime( $filePath ) {
+ MediaWiki\suppressWarnings();
+ $mtime = filemtime( $filePath ) ?: 1;
+ MediaWiki\restoreWarnings();
return $mtime;
}
+
+ /**
+ * Compute a non-cryptographic string hash of a file's contents.
+ * If the file does not exist or cannot be read, returns an empty string.
+ *
+ * @since 1.26 Uses MD4 instead of SHA1.
+ * @param string $filePath File path
+ * @return string Hash
+ */
+ protected static function safeFileHash( $filePath ) {
+ static $cache;
+
+ if ( !$cache ) {
+ $cache = ObjectCache::newAccelerator( CACHE_NONE );
+ }
+
+ MediaWiki\suppressWarnings();
+ $mtime = filemtime( $filePath );
+ MediaWiki\restoreWarnings();
+ if ( !$mtime ) {
+ return '';
+ }
+
+ $cacheKey = wfGlobalCacheKey( 'resourceloader', __METHOD__, $filePath );
+ $cachedHash = $cache->get( $cacheKey );
+ if ( isset( $cachedHash['mtime'] ) && $cachedHash['mtime'] === $mtime ) {
+ return $cachedHash['hash'];
+ }
+
+ MediaWiki\suppressWarnings();
+ $contents = file_get_contents( $filePath );
+ MediaWiki\restoreWarnings();
+ if ( !$contents ) {
+ return '';
+ }
+
+ $hash = hash( 'md4', $contents );
+ $cache->set( $cacheKey, array( 'mtime' => $mtime, 'hash' => $hash ), 60 * 60 * 24 );
+
+ return $hash;
+ }
}
diff --git a/includes/resourceloader/ResourceLoaderOOUIImageModule.php b/includes/resourceloader/ResourceLoaderOOUIImageModule.php
new file mode 100644
index 00000000..8493f9fd
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderOOUIImageModule.php
@@ -0,0 +1,86 @@
+<?php
+/**
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+/**
+ * Secret special sauce.
+ *
+ * @since 1.26
+ */
+class ResourceLoaderOOUIImageModule extends ResourceLoaderImageModule {
+ protected function loadFromDefinition() {
+ if ( $this->definition === null ) {
+ return;
+ }
+
+ // Core default themes
+ $themes = array( 'default' => 'mediawiki' );
+ $themes += ExtensionRegistry::getInstance()->getAttribute( 'SkinOOUIThemes' );
+
+ $name = $this->definition['name'];
+ $rootPath = $this->definition['rootPath'];
+
+ $definition = array();
+ foreach ( $themes as $skin => $theme ) {
+ // TODO Allow extensions to specify this path somehow
+ $dataPath = $this->localBasePath . '/' . $rootPath . '/' . $theme . '/' . $name . '.json';
+
+ if ( file_exists( $dataPath ) ) {
+ $data = json_decode( file_get_contents( $dataPath ), true );
+ $fixPath = function ( &$path ) use ( $rootPath, $theme ) {
+ // TODO Allow extensions to specify this path somehow
+ $path = $rootPath . '/' . $theme . '/' . $path;
+ };
+ array_walk( $data['images'], function ( &$value ) use ( $fixPath ) {
+ if ( is_string( $value['file'] ) ) {
+ $fixPath( $value['file'] );
+ } elseif ( is_array( $value['file'] ) ) {
+ array_walk_recursive( $value['file'], $fixPath );
+ }
+ } );
+ } else {
+ $data = array();
+ }
+
+ foreach ( $data as $key => $value ) {
+ switch ( $key ) {
+ case 'images':
+ case 'variants':
+ $definition[$key][$skin] = $data[$key];
+ break;
+
+ default:
+ if ( !isset( $definition[$key] ) ) {
+ $definition[$key] = $data[$key];
+ } elseif ( $definition[$key] !== $data[$key] ) {
+ throw new Exception(
+ "Mismatched OOUI theme definitions are not supported: trying to load $key of $theme theme"
+ );
+ }
+ break;
+ }
+ }
+ }
+
+ // Fields from definition silently override keys from JSON files
+ $this->definition += $definition;
+
+ parent::loadFromDefinition();
+ }
+}
diff --git a/includes/resourceloader/ResourceLoaderRawFileModule.php b/includes/resourceloader/ResourceLoaderRawFileModule.php
new file mode 100644
index 00000000..d9005fa5
--- /dev/null
+++ b/includes/resourceloader/ResourceLoaderRawFileModule.php
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Module containing files that are loaded without ResourceLoader.
+ *
+ * Primary usecase being "base" modules loaded by the startup module,
+ * such as jquery and the mw.loader client itself. These make use of
+ * ResourceLoaderModule and load.php for convenience but aren't actually
+ * registered in the startup module (as it would have to load itself).
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @author Timo Tijhof
+ */
+
+class ResourceLoaderRawFileModule extends ResourceLoaderFileModule {
+
+ /**
+ * Enable raw mode to omit mw.loader.state() call as mw.loader
+ * does not yet exist when these modules execute.
+ * @var boolean
+ */
+ protected $raw = true;
+
+ /**
+ * Get all JavaScript code.
+ *
+ * @param ResourceLoaderContext $context
+ * @return string JavaScript code
+ */
+ public function getScript( ResourceLoaderContext $context ) {
+ $script = parent::getScript( $context );
+ // Add closure explicitly because raw modules can't be wrapped mw.loader.implement.
+ // Unlike with mw.loader.implement, this closure is immediately invoked.
+ // @see ResourceLoader::makeModuleResponse
+ // @see ResourceLoader::makeLoaderImplementScript
+ return "(function () {\n{$script}\n}());";
+ }
+}
diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php
index 19e0baeb..380b7a53 100644
--- a/includes/resourceloader/ResourceLoaderSiteModule.php
+++ b/includes/resourceloader/ResourceLoaderSiteModule.php
@@ -47,13 +47,4 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
}
return $pages;
}
-
- /**
- * Get group name
- *
- * @return string
- */
- public function getGroup() {
- return 'site';
- }
}
diff --git a/includes/resourceloader/ResourceLoaderSkinModule.php b/includes/resourceloader/ResourceLoaderSkinModule.php
index 3ba63e68..911d9534 100644
--- a/includes/resourceloader/ResourceLoaderSkinModule.php
+++ b/includes/resourceloader/ResourceLoaderSkinModule.php
@@ -44,13 +44,13 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule {
'(min-resolution: 1.5dppx), ' .
'(min-resolution: 144dpi)'
][] = '.mw-wiki-logo { background-image: ' .
- CSSMin::buildUrlValue( $logoHD['1.5x'] ) .';' .
+ CSSMin::buildUrlValue( $logoHD['1.5x'] ) . ';' .
'background-size: 135px auto; }';
}
if ( isset( $logoHD['2x'] ) ) {
$styles[
'(-webkit-min-device-pixel-ratio: 2), ' .
- '(min--moz-device-pixel-ratio: 2),'.
+ '(min--moz-device-pixel-ratio: 2),' .
'(min-resolution: 2dppx), ' .
'(min-resolution: 192dpi)'
][] = '.mw-wiki-logo { background-image: ' .
@@ -73,15 +73,6 @@ class ResourceLoaderSkinModule extends ResourceLoaderFileModule {
/**
* @param $context ResourceLoaderContext
- * @return int|mixed
- */
- public function getModifiedTime( ResourceLoaderContext $context ) {
- $parentMTime = parent::getModifiedTime( $context );
- return max( $parentMTime, $this->getHashMtime( $context ) );
- }
-
- /**
- * @param $context ResourceLoaderContext
* @return string: Hash
*/
public function getModifiedHash( ResourceLoaderContext $context ) {
diff --git a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php
index 5c917091..8170cb1c 100644
--- a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php
+++ b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php
@@ -35,7 +35,8 @@ class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule {
* @return array
*/
protected function getData() {
- return json_decode( file_get_contents( $this->path ) );
+ global $IP;
+ return json_decode( file_get_contents( "$IP/{$this->path}" ) );
}
/**
@@ -53,25 +54,17 @@ class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule {
}
/**
- * @param ResourceLoaderContext $context
- * @return int UNIX timestamp
+ * @return bool
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
- return static::safeFilemtime( $this->path );
+ public function enableModuleContentVersion() {
+ return true;
}
/**
* @param ResourceLoaderContext $context
- * @return string Hash
- */
- public function getModifiedHash( ResourceLoaderContext $context ) {
- return md5( serialize( $this->getData() ) );
- }
-
- /**
* @return array
*/
- public function getDependencies() {
+ public function getDependencies( ResourceLoaderContext $context = null ) {
return array( 'mediawiki.language' );
}
diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php
index b2fbae9c..87d8ee20 100644
--- a/includes/resourceloader/ResourceLoaderStartUpModule.php
+++ b/includes/resourceloader/ResourceLoaderStartUpModule.php
@@ -24,14 +24,10 @@
class ResourceLoaderStartUpModule extends ResourceLoaderModule {
- /* Protected Members */
-
- protected $modifiedTime = array();
+ // Cache for getConfigSettings() as it's called by multiple methods
protected $configVars = array();
protected $targets = array( 'desktop', 'mobile' );
- /* Protected Methods */
-
/**
* @param ResourceLoaderContext $context
* @return array
@@ -92,6 +88,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgContentNamespaces' => MWNamespace::getContentNamespaces(),
'wgSiteName' => $conf->get( 'Sitename' ),
'wgDBname' => $conf->get( 'DBname' ),
+ 'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
'wgAvailableSkins' => Skin::getSkinNames(),
'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
// MediaWiki sets cookies to have this prefix by default
@@ -104,6 +101,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ),
'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
+ 'wgResourceLoaderLegacyModules' => self::getLegacyModules(),
+ 'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
+ 'wgEnableUploads' => $conf->get( 'EnableUploads' ),
);
Hooks::run( 'ResourceLoaderGetConfigVars', array( &$vars ) );
@@ -159,7 +159,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
* data send to the client.
*
* @param array &$registryData Modules keyed by name with properties:
- * - number 'version'
+ * - string 'version'
* - array 'dependencies'
* - string|null 'group'
* - string 'source'
@@ -191,6 +191,9 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
$resourceLoader = $context->getResourceLoader();
$target = $context->getRequest()->getVal( 'target', 'desktop' );
+ // Bypass target filter if this request is from a unit test context. To prevent misuse in
+ // production, this is only allowed if testing is enabled server-side.
+ $byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
$out = '';
$registryData = array();
@@ -199,7 +202,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
foreach ( $resourceLoader->getModuleNames() as $name ) {
$module = $resourceLoader->getModule( $name );
$moduleTargets = $module->getTargets();
- if ( !in_array( $target, $moduleTargets ) ) {
+ if ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) ) {
continue;
}
@@ -210,31 +213,27 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
continue;
}
- // Coerce module timestamp to UNIX timestamp.
- // getModifiedTime() is supposed to return a UNIX timestamp, but custom implementations
- // might forget. TODO: Maybe emit warning?
- $moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) );
+ $versionHash = $module->getVersionHash( $context );
+ if ( strlen( $versionHash ) !== 8 ) {
+ // Module implementation either broken or deviated from ResourceLoader::makeHash
+ // Asserted by tests/phpunit/structure/ResourcesTest.
+ $versionHash = ResourceLoader::makeHash( $versionHash );
+ }
$skipFunction = $module->getSkipFunction();
if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) {
$skipFunction = $resourceLoader->filter( 'minify-js',
$skipFunction,
- // There will potentially be lots of these little string in the registrations
+ // There will potentially be lots of these little strings in the registrations
// manifest, we don't want to blow up the startup module with
- // "/* cache key: ... */" all over it in non-debug mode.
+ // "/* cache key: ... */" all over it.
/* cacheReport = */ false
);
}
- $mtime = max(
- $moduleMtime,
- wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) )
- );
-
$registryData[$name] = array(
- // Convert to numbers as wfTimestamp always returns a string, even for TS_UNIX
- 'version' => (int) $mtime,
- 'dependencies' => $module->getDependencies(),
+ 'version' => $versionHash,
+ 'dependencies' => $module->getDependencies( $context ),
'group' => $module->getGroup(),
'source' => $module->getSource(),
'loader' => $module->getLoaderScript(),
@@ -263,7 +262,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
continue;
}
- // Call mw.loader.register(name, timestamp, dependencies, group, source, skip)
+ // Call mw.loader.register(name, version, dependencies, group, source, skip)
$registrations[] = array(
$name,
$data['version'],
@@ -276,13 +275,11 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
}
// Register modules
- $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
+ $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations );
return $out;
}
- /* Methods */
-
/**
* @return bool
*/
@@ -299,6 +296,20 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
return array( 'jquery', 'mediawiki' );
}
+ public static function getLegacyModules() {
+ global $wgIncludeLegacyJavaScript, $wgPreloadJavaScriptMwUtil;
+
+ $legacyModules = array();
+ if ( $wgIncludeLegacyJavaScript ) {
+ $legacyModules[] = 'mediawiki.legacy.wikibits';
+ }
+ if ( $wgPreloadJavaScriptMwUtil ) {
+ $legacyModules[] = 'mediawiki.util';
+ }
+
+ return $legacyModules;
+ }
+
/**
* Get the load URL of the startup modules.
*
@@ -309,24 +320,16 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
* @return string
*/
public static function getStartupModulesUrl( ResourceLoaderContext $context ) {
+ $rl = $context->getResourceLoader();
$moduleNames = self::getStartupModules();
- // Get the latest version
- $loader = $context->getResourceLoader();
- $version = 1;
- foreach ( $moduleNames as $moduleName ) {
- $version = max( $version,
- $loader->getModule( $moduleName )->getModifiedTime( $context )
- );
- }
-
$query = array(
'modules' => ResourceLoader::makePackedModulesString( $moduleNames ),
'only' => 'scripts',
'lang' => $context->getLanguage(),
'skin' => $context->getSkin(),
'debug' => $context->getDebug() ? 'true' : 'false',
- 'version' => wfTimestamp( TS_ISO_8601_BASIC, $version )
+ 'version' => $rl->getCombinedVersion( $context, $moduleNames ),
);
// Ensure uniform query order
ksort( $query );
@@ -339,40 +342,25 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
*/
public function getScript( ResourceLoaderContext $context ) {
global $IP;
+ if ( $context->getOnly() !== 'scripts' ) {
+ return '/* Requires only=script */';
+ }
$out = file_get_contents( "$IP/resources/src/startup.js" );
- if ( $context->getOnly() === 'scripts' ) {
- // Startup function
- $configuration = $this->getConfigSettings( $context );
- $registrations = $this->getModuleRegistrations( $context );
+ $pairs = array_map( function ( $value ) {
+ $value = FormatJson::encode( $value, ResourceLoader::inDebugMode(), FormatJson::ALL_OK );
// Fix indentation
- $registrations = str_replace( "\n", "\n\t", trim( $registrations ) );
- $mwMapJsCall = Xml::encodeJsCall(
- 'mw.Map',
- array( $this->getConfig()->get( 'LegacyJavaScriptGlobals' ) )
- );
- $mwConfigSetJsCall = Xml::encodeJsCall(
- 'mw.config.set',
- array( $configuration ),
- ResourceLoader::inDebugMode()
- );
-
- $out .= "var startUp = function () {\n" .
- "\tmw.config = new " .
- $mwMapJsCall . "\n" .
- "\t$registrations\n" .
- "\t" . $mwConfigSetJsCall .
- "};\n";
-
- // Conditional script injection
- $scriptTag = Html::linkedScript( self::getStartupModulesUrl( $context ) );
- $out .= "if ( isCompatible() ) {\n" .
- "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) .
- "\n}";
- }
-
- return $out;
+ $value = str_replace( "\n", "\n\t", $value );
+ return $value;
+ }, array(
+ '$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
+ '$VARS.configuration' => $this->getConfigSettings( $context ),
+ '$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ),
+ ) );
+ $pairs['$CODE.registrations()'] = str_replace( "\n", "\n\t", trim( $this->getModuleRegistrations( $context ) ) );
+
+ return strtr( $out, $pairs );
}
/**
@@ -383,59 +371,48 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule {
}
/**
+ * Get the definition summary for this module.
+ *
* @param ResourceLoaderContext $context
- * @return array|mixed
+ * @return array
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
+ public function getDefinitionSummary( ResourceLoaderContext $context ) {
global $IP;
+ $summary = parent::getDefinitionSummary( $context );
+ $summary[] = array(
+ // Detect changes to variables exposed in mw.config (T30899).
+ 'vars' => $this->getConfigSettings( $context ),
+ // Changes how getScript() creates mw.Map for mw.config
+ 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
+ // Detect changes to the module registrations
+ 'moduleHashes' => $this->getAllModuleHashes( $context ),
- $hash = $context->getHash();
- if ( isset( $this->modifiedTime[$hash] ) ) {
- return $this->modifiedTime[$hash];
- }
-
- // Call preloadModuleInfo() on ALL modules as we're about
- // to call getModifiedTime() on all of them
- $loader = $context->getResourceLoader();
- $loader->preloadModuleInfo( $loader->getModuleNames(), $context );
-
- $time = max(
- wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ),
- filemtime( "$IP/resources/src/startup.js" ),
- $this->getHashMtime( $context )
+ 'fileMtimes' => array(
+ filemtime( "$IP/resources/src/startup.js" ),
+ ),
);
-
- // ATTENTION!: Because of the line below, this is not going to cause
- // infinite recursion - think carefully before making changes to this
- // code!
- // Pre-populate modifiedTime with something because the loop over
- // all modules below includes the startup module (this module).
- $this->modifiedTime[$hash] = 1;
-
- foreach ( $loader->getModuleNames() as $name ) {
- $module = $loader->getModule( $name );
- $time = max( $time, $module->getModifiedTime( $context ) );
- }
-
- $this->modifiedTime[$hash] = $time;
- return $this->modifiedTime[$hash];
+ return $summary;
}
/**
- * Hash of all dynamic data embedded in getScript().
- *
- * Detect changes to mw.config settings embedded in #getScript (bug 28899).
+ * Helper method for getDefinitionSummary().
*
* @param ResourceLoaderContext $context
- * @return string Hash
+ * @return string SHA-1
*/
- public function getModifiedHash( ResourceLoaderContext $context ) {
- $data = array(
- 'vars' => $this->getConfigSettings( $context ),
- 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
- );
-
- return md5( serialize( $data ) );
+ protected function getAllModuleHashes( ResourceLoaderContext $context ) {
+ $rl = $context->getResourceLoader();
+ // Preload for getCombinedVersion()
+ $rl->preloadModuleInfo( $rl->getModuleNames(), $context );
+
+ // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
+ // Think carefully before making changes to this code!
+ // Pre-populate versionHash with something because the loop over all modules below includes
+ // the startup module (this module).
+ // See ResourceLoaderModule::getVersionHash() for usage of this cache.
+ $this->versionHash[$context->getHash()] = null;
+
+ return $rl->getCombinedVersion( $context, $rl->getModuleNames() );
}
/**
diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
index 472ceb26..65d770e2 100644
--- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php
@@ -27,25 +27,13 @@
*/
class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule {
- /* Protected Members */
-
- protected $modifiedTime = array();
-
protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
- /* Methods */
-
/**
- * @param ResourceLoaderContext $context
- * @return array|int|mixed
+ * @return bool
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
- $hash = $context->getHash();
- if ( !isset( $this->modifiedTime[$hash] ) ) {
- $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $context->getUserObj()->getTouched() );
- }
-
- return $this->modifiedTime[$hash];
+ public function enableModuleContentVersion() {
+ return true;
}
/**
diff --git a/includes/resourceloader/ResourceLoaderUserDefaultsModule.php b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php
index 5f4bc16b..eba61edc 100644
--- a/includes/resourceloader/ResourceLoaderUserDefaultsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php
@@ -26,26 +26,13 @@
*/
class ResourceLoaderUserDefaultsModule extends ResourceLoaderModule {
- /* Protected Members */
-
protected $targets = array( 'desktop', 'mobile' );
- /* Methods */
-
- /**
- * @param ResourceLoaderContext $context
- * @return string Hash
- */
- public function getModifiedHash( ResourceLoaderContext $context ) {
- return md5( serialize( User::getDefaultOptions() ) );
- }
-
/**
- * @param ResourceLoaderContext $context
- * @return int
+ * @return bool
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
- return $this->getHashMtime( $context );
+ public function enableModuleContentVersion() {
+ return true;
}
/**
diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
index 84c1906d..0847109c 100644
--- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php
+++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php
@@ -27,34 +27,23 @@
*/
class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
- /* Protected Members */
-
- protected $modifiedTime = array();
-
protected $origin = self::ORIGIN_CORE_INDIVIDUAL;
protected $targets = array( 'desktop', 'mobile' );
- /* Methods */
-
/**
+ * @param ResourceLoaderContext $context
* @return array List of module names as strings
*/
- public function getDependencies() {
+ public function getDependencies( ResourceLoaderContext $context = null ) {
return array( 'user.defaults' );
}
/**
- * @param ResourceLoaderContext $context
- * @return int
+ * @return bool
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
- $hash = $context->getHash();
- if ( !isset( $this->modifiedTime[$hash] ) ) {
- $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $context->getUserObj()->getTouched() );
- }
-
- return $this->modifiedTime[$hash];
+ public function enableModuleContentVersion() {
+ return true;
}
/**
diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php
index 7b44cc67..0023de27 100644
--- a/includes/resourceloader/ResourceLoaderWikiModule.php
+++ b/includes/resourceloader/ResourceLoaderWikiModule.php
@@ -26,15 +26,30 @@
* Abstraction for resource loader modules which pull from wiki pages
*
* This can only be used for wiki pages in the MediaWiki and User namespaces,
- * because of its dependence on the functionality of
- * Title::isCssJsSubpage.
+ * because of its dependence on the functionality of Title::isCssJsSubpage.
+ *
+ * This module supports being used as a placeholder for a module on a remote wiki.
+ * To do so, getDB() must be overloaded to return a foreign database object that
+ * allows local wikis to query page metadata.
+ *
+ * Safe for calls on local wikis are:
+ * - Option getters:
+ * - getGroup()
+ * - getPosition()
+ * - getPages()
+ * - Basic methods that strictly involve the foreign database
+ * - getDB()
+ * - isKnownEmpty()
+ * - getTitleInfo()
*/
class ResourceLoaderWikiModule extends ResourceLoaderModule {
+ /** @var string Position on the page to load this module at */
+ protected $position = 'bottom';
// Origin defaults to users with sitewide authority
protected $origin = self::ORIGIN_USER_SITEWIDE;
- // In-object cache for title info
+ // In-process cache for title info
protected $titleInfo = array();
// List of page names that contain CSS
@@ -50,14 +65,21 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
* @param array $options For back-compat, this can be omitted in favour of overwriting getPages.
*/
public function __construct( array $options = null ) {
- if ( isset( $options['styles'] ) ) {
- $this->styles = $options['styles'];
+ if ( is_null( $options ) ) {
+ return;
}
- if ( isset( $options['scripts'] ) ) {
- $this->scripts = $options['scripts'];
- }
- if ( isset( $options['group'] ) ) {
- $this->group = $options['group'];
+
+ foreach ( $options as $member => $option ) {
+ switch ( $member ) {
+ case 'position':
+ $this->isPositionDefined = true;
+ // Don't break since we need the member set as well
+ case 'styles':
+ case 'scripts':
+ case 'group':
+ $this->{$member} = $option;
+ break;
+ }
}
}
@@ -107,13 +129,13 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
}
/**
- * Get the Database object used in getTitleMTimes(). Defaults to the local slave DB
- * but subclasses may want to override this to return a remote DB object, or to return
- * null if getTitleMTimes() shouldn't access the DB at all.
+ * Get the Database object used in getTitleInfo().
+ *
+ * Defaults to the local slave DB. Subclasses may want to override this to return a foreign
+ * database object, or null if getTitleInfo() shouldn't access the database.
*
- * NOTE: This ONLY works for getTitleMTimes() and getModifiedTime(), NOT FOR ANYTHING ELSE.
- * In particular, it doesn't work for getting the content of JS and CSS pages. That functionality
- * will use the local DB irrespective of the return value of this method.
+ * NOTE: This ONLY works for getTitleInfo() and isKnownEmpty(), NOT FOR ANYTHING ELSE.
+ * In particular, it doesn't work for getContent() or getScript() etc.
*
* @return IDatabase|null
*/
@@ -122,10 +144,15 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
}
/**
- * @param Title $title
+ * @param string $title
* @return null|string
*/
- protected function getContent( $title ) {
+ protected function getContent( $titleText ) {
+ $title = Title::newFromText( $titleText );
+ if ( !$title ) {
+ return null;
+ }
+
$handler = ContentHandler::getForTitle( $title );
if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) {
$format = CONTENT_FORMAT_CSS;
@@ -160,11 +187,7 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
if ( $options['type'] !== 'script' ) {
continue;
}
- $title = Title::newFromText( $titleText );
- if ( !$title || $title->isRedirect() ) {
- continue;
- }
- $script = $this->getContent( $title );
+ $script = $this->getContent( $titleText );
if ( strval( $script ) !== '' ) {
$script = $this->validateScriptFile( $titleText, $script );
$scripts .= ResourceLoader::makeComment( $titleText ) . $script . "\n";
@@ -183,12 +206,8 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
if ( $options['type'] !== 'style' ) {
continue;
}
- $title = Title::newFromText( $titleText );
- if ( !$title || $title->isRedirect() ) {
- continue;
- }
$media = isset( $options['media'] ) ? $options['media'] : 'all';
- $style = $this->getContent( $title );
+ $style = $this->getContent( $titleText );
if ( strval( $style ) === '' ) {
continue;
}
@@ -206,37 +225,31 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
}
/**
- * @param ResourceLoaderContext $context
- * @return int
+ * Disable module content versioning.
+ *
+ * This class does not support generating content outside of a module
+ * request due to foreign database support.
+ *
+ * See getDefinitionSummary() for meta-data versioning.
+ *
+ * @return bool
*/
- public function getModifiedTime( ResourceLoaderContext $context ) {
- $modifiedTime = 1;
- $titleInfo = $this->getTitleInfo( $context );
- if ( count( $titleInfo ) ) {
- $mtimes = array_map( function ( $value ) {
- return $value['timestamp'];
- }, $titleInfo );
- $modifiedTime = max( $modifiedTime, max( $mtimes ) );
- }
- $modifiedTime = max(
- $modifiedTime,
- $this->getMsgBlobMtime( $context->getLanguage() ),
- $this->getDefinitionMtime( $context )
- );
- return $modifiedTime;
+ public function enableModuleContentVersion() {
+ return false;
}
/**
- * Get the definition summary for this module.
- *
* @param ResourceLoaderContext $context
* @return array
*/
public function getDefinitionSummary( ResourceLoaderContext $context ) {
- return array(
- 'class' => get_class( $this ),
+ $summary = parent::getDefinitionSummary( $context );
+ $summary[] = array(
'pages' => $this->getPages( $context ),
+ // Includes SHA1 of content
+ 'titleInfo' => $this->getTitleInfo( $context ),
);
+ return $summary;
}
/**
@@ -244,33 +257,29 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
* @return bool
*/
public function isKnownEmpty( ResourceLoaderContext $context ) {
- $titleInfo = $this->getTitleInfo( $context );
- // Bug 68488: For modules in the "user" group, we should actually
- // check that the pages are empty (page_len == 0), but for other
- // groups, just check the pages exist so that we don't end up
- // caching temporarily-blank pages without the appropriate
- // <script> or <link> tag.
- if ( $this->getGroup() !== 'user' ) {
- return count( $titleInfo ) === 0;
- }
+ $revisions = $this->getTitleInfo( $context );
- foreach ( $titleInfo as $info ) {
- if ( $info['length'] !== 0 ) {
- // At least one non-0-lenth page, not empty
- return false;
+ // For user modules, don't needlessly load if there are no non-empty pages
+ if ( $this->getGroup() === 'user' ) {
+ foreach ( $revisions as $revision ) {
+ if ( $revision['rev_len'] > 0 ) {
+ // At least one non-empty page, module should be loaded
+ return false;
+ }
}
+ return true;
}
- // All pages are 0-length, so it's empty
- return true;
+ // Bug 68488: For other modules (i.e. ones that are called in cached html output) only check
+ // page existance. This ensures that, if some pages in a module are temporarily blanked,
+ // we don't end omit the module's script or link tag on some pages.
+ return count( $revisions ) === 0;
}
/**
- * Get the modification times of all titles that would be loaded for
- * a given context.
- * @param ResourceLoaderContext $context Context object
- * @return array Keyed by page dbkey. Value is an array with 'length' and 'timestamp'
- * keys, where the timestamp is a UNIX timestamp
+ * Get the information about the wiki pages for a given context.
+ * @param ResourceLoaderContext $context
+ * @return array Keyed by page name. Contains arrays with 'rev_len' and 'rev_sha1' keys
*/
protected function getTitleInfo( ResourceLoaderContext $context ) {
$dbr = $this->getDB();
@@ -279,31 +288,38 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
return array();
}
- $hash = $context->getHash();
- if ( isset( $this->titleInfo[$hash] ) ) {
- return $this->titleInfo[$hash];
- }
-
- $this->titleInfo[$hash] = array();
- $batch = new LinkBatch;
- foreach ( $this->getPages( $context ) as $titleText => $options ) {
- $batch->addObj( Title::newFromText( $titleText ) );
- }
+ $pages = $this->getPages( $context );
+ $key = implode( '|', array_keys( $pages ) );
+ if ( !isset( $this->titleInfo[$key] ) ) {
+ $this->titleInfo[$key] = array();
+ $batch = new LinkBatch;
+ foreach ( $pages as $titleText => $options ) {
+ $batch->addObj( Title::newFromText( $titleText ) );
+ }
- if ( !$batch->isEmpty() ) {
- $res = $dbr->select( 'page',
- array( 'page_namespace', 'page_title', 'page_touched', 'page_len' ),
- $batch->constructSet( 'page', $dbr ),
- __METHOD__
- );
- foreach ( $res as $row ) {
- $title = Title::makeTitle( $row->page_namespace, $row->page_title );
- $this->titleInfo[$hash][$title->getPrefixedDBkey()] = array(
- 'timestamp' => wfTimestamp( TS_UNIX, $row->page_touched ),
- 'length' => $row->page_len,
+ if ( !$batch->isEmpty() ) {
+ $res = $dbr->select( array( 'page', 'revision' ),
+ array( 'page_namespace', 'page_title', 'rev_len', 'rev_sha1' ),
+ $batch->constructSet( 'page', $dbr ),
+ __METHOD__,
+ array(),
+ array( 'revision' => array( 'INNER JOIN', array( 'page_latest=rev_id' ) ) )
);
+ foreach ( $res as $row ) {
+ // Avoid including ids or timestamps of revision/page tables so
+ // that versions are not wasted
+ $title = Title::makeTitle( $row->page_namespace, $row->page_title );
+ $this->titleInfo[$key][$title->getPrefixedText()] = array(
+ 'rev_len' => $row->rev_len,
+ 'rev_sha1' => $row->rev_sha1,
+ );
+ }
}
}
- return $this->titleInfo[$hash];
+ return $this->titleInfo[$key];
+ }
+
+ public function getPosition() {
+ return $this->position;
}
}