diff options
Diffstat (limited to 'includes/resourceloader')
22 files changed, 1550 insertions, 410 deletions
diff --git a/includes/resourceloader/DerivativeResourceLoaderContext.php b/includes/resourceloader/DerivativeResourceLoaderContext.php index d114d7ed..5784f2a0 100644 --- a/includes/resourceloader/DerivativeResourceLoaderContext.php +++ b/includes/resourceloader/DerivativeResourceLoaderContext.php @@ -126,6 +126,7 @@ class DerivativeResourceLoaderContext extends ResourceLoaderContext { public function setUser( $user ) { $this->user = $user; $this->hash = null; + $this->userObj = null; } public function getDebug() { diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 4f1414bc..150ccd07 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -35,26 +35,47 @@ class ResourceLoader { /** @var bool */ protected static $debugMode = null; - /** @var array Module name/ResourceLoaderModule object pairs */ + /** @var array */ + private static $lessVars = null; + + /** + * Module name/ResourceLoaderModule object pairs + * @var array + */ protected $modules = array(); - /** @var array Associative array mapping module name to info associative array */ + /** + * Associative array mapping module name to info associative array + * @var array + */ protected $moduleInfos = array(); /** @var Config $config */ private $config; /** - * @var array Associative array mapping framework ids to a list of names of test suite modules - * like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) + * Associative array mapping framework ids to a list of names of test suite modules + * like array( 'qunit' => array( 'mediawiki.tests.qunit.suites', 'ext.foo.tests', .. ), .. ) + * @var array */ protected $testModuleNames = array(); - /** @var array E.g. array( 'source-id' => 'http://.../load.php' ) */ + /** + * E.g. array( 'source-id' => 'http://.../load.php' ) + * @var array + */ protected $sources = array(); - /** @var bool */ - protected $hasErrors = false; + /** + * Errors accumulated during current respond() call. + * @var array + */ + protected $errors = array(); + + /** + * @var MessageBlobStore + */ + protected $blobStore; /** * Load information stored in the database about modules. @@ -130,7 +151,7 @@ class ResourceLoader { foreach ( array_keys( $modulesWithoutMessages ) as $name ) { $module = $this->getModule( $name ); if ( $module ) { - $module->setMsgBlobMtime( $lang, 0 ); + $module->setMsgBlobMtime( $lang, 1 ); } } } @@ -152,12 +173,10 @@ class ResourceLoader { * @return string Filtered data, or a comment containing an error message */ public function filter( $filter, $data, $cacheReport = true ) { - wfProfileIn( __METHOD__ ); // 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' ) ) ) { - wfProfileOut( __METHOD__ ); return $data; } @@ -168,7 +187,6 @@ class ResourceLoader { $cacheEntry = $cache->get( $key ); if ( is_string( $cacheEntry ) ) { wfIncrStats( "rl-$filter-cache-hits" ); - wfProfileOut( __METHOD__ ); return $cacheEntry; } @@ -199,13 +217,9 @@ class ResourceLoader { } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); wfDebugLog( 'resourceloader', __METHOD__ . ": minification failed: $e" ); - $this->hasErrors = true; - // Return exception as a comment - $result = self::formatException( $e ); + $this->errors[] = self::formatExceptionNoComment( $e ); } - wfProfileOut( __METHOD__ ); - return $result; } @@ -218,8 +232,6 @@ class ResourceLoader { public function __construct( Config $config = null ) { global $IP; - wfProfileIn( __METHOD__ ); - if ( $config === null ) { wfDebug( __METHOD__ . ' was called without providing a Config instance' ); $config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); @@ -236,14 +248,14 @@ class ResourceLoader { // Register core modules $this->register( include "$IP/resources/Resources.php" ); // Register extension modules - wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) ); + Hooks::run( 'ResourceLoaderRegisterModules', array( &$this ) ); $this->register( $config->get( 'ResourceModules' ) ); if ( $config->get( 'EnableJavaScriptTest' ) === true ) { $this->registerTestModules(); } - wfProfileOut( __METHOD__ ); + $this->setMessageBlobStore( new MessageBlobStore() ); } /** @@ -254,6 +266,14 @@ class ResourceLoader { } /** + * @param MessageBlobStore $blobStore + * @since 1.25 + */ + public function setMessageBlobStore( MessageBlobStore $blobStore ) { + $this->blobStore = $blobStore; + } + + /** * Register a module with the ResourceLoader system. * * @param mixed $name Name of module as a string or List of name/object pairs as an array @@ -267,14 +287,12 @@ class ResourceLoader { * not registered */ public function register( $name, $info = null ) { - wfProfileIn( __METHOD__ ); // Allow multiple modules to be registered in one call $registrations = is_array( $name ) ? $name : array( $name => $info ); foreach ( $registrations as $name => $info ) { // Disallow duplicate registrations if ( isset( $this->moduleInfos[$name] ) ) { - wfProfileOut( __METHOD__ ); // A module has already been registered by this name throw new MWException( 'ResourceLoader duplicate registration error. ' . @@ -284,7 +302,6 @@ class ResourceLoader { // Check $name for validity if ( !self::isValidModuleName( $name ) ) { - wfProfileOut( __METHOD__ ); throw new MWException( "ResourceLoader module name '$name' is invalid, " . "see ResourceLoader::isValidModuleName()" ); } @@ -298,7 +315,6 @@ class ResourceLoader { // New calling convention $this->moduleInfos[$name] = $info; } else { - wfProfileOut( __METHOD__ ); throw new MWException( 'ResourceLoader module info type error for module \'' . $name . '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')' @@ -323,19 +339,16 @@ class ResourceLoader { } elseif ( isset( $skinStyles['+' . $name] ) ) { $paths = (array)$skinStyles['+' . $name]; $styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ? - $this->moduleInfos[$name]['skinStyles']['default'] : + (array)$this->moduleInfos[$name]['skinStyles']['default'] : array(); } else { continue; } // Add new file paths, remapping them to refer to our directories and not use settings - // from the module we're modifying. These can come from the base definition or be defined - // for each module. + // from the module we're modifying, which come from the base definition. list( $localBasePath, $remoteBasePath ) = ResourceLoaderFileModule::extractBasePaths( $skinStyles ); - list( $localBasePath, $remoteBasePath ) = - ResourceLoaderFileModule::extractBasePaths( $paths, $localBasePath, $remoteBasePath ); foreach ( $paths as $path ) { $styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath ); @@ -346,7 +359,6 @@ class ResourceLoader { } } - wfProfileOut( __METHOD__ ); } /** @@ -360,13 +372,11 @@ class ResourceLoader { . 'Edit your <code>LocalSettings.php</code> to enable it.' ); } - wfProfileIn( __METHOD__ ); - // Get core test suites $testModules = array(); $testModules['qunit'] = array(); // Get other test suites (e.g. from extensions) - wfRunHooks( 'ResourceLoaderTestModules', array( &$testModules, &$this ) ); + Hooks::run( 'ResourceLoaderTestModules', array( &$testModules, &$this ) ); // Add the testrunner (which configures QUnit) to the dependencies. // Since it must be ready before any of the test suites are executed. @@ -389,7 +399,6 @@ class ResourceLoader { $this->testModuleNames[$id] = array_keys( $testModules[$id] ); } - wfProfileOut( __METHOD__ ); } /** @@ -464,6 +473,17 @@ class ResourceLoader { } /** + * Check whether a ResourceLoader module is registered + * + * @since 1.25 + * @param string $name + * @return bool + */ + public function isModuleRegistered( $name ) { + return isset( $this->moduleInfos[$name] ); + } + + /** * Get the ResourceLoaderModule object for a given module name. * * If an array of module parameters exists but a ResourceLoaderModule object has not @@ -568,9 +588,6 @@ class ResourceLoader { // See http://bugs.php.net/bug.php?id=36514 ob_start(); - wfProfileIn( __METHOD__ ); - $errors = ''; - // Find out which modules are missing and instantiate the others $modules = array(); $missing = array(); @@ -581,10 +598,7 @@ class ResourceLoader { // This is a security issue, see bug 34907. if ( $module->getGroup() === 'private' ) { wfDebugLog( 'resourceloader', __METHOD__ . ": request for private module '$name' denied" ); - $this->hasErrors = true; - // Add exception to the output as a comment - $errors .= self::makeComment( "Cannot show private module \"$name\"" ); - + $this->errors[] = "Cannot show private module \"$name\""; continue; } $modules[$name] = $module; @@ -599,13 +613,9 @@ class ResourceLoader { } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); wfDebugLog( 'resourceloader', __METHOD__ . ": preloading module info failed: $e" ); - $this->hasErrors = true; - // Add exception to the output as a comment - $errors .= self::formatException( $e ); + $this->errors[] = self::formatExceptionNoComment( $e ); } - wfProfileIn( __METHOD__ . '-getModifiedTime' ); - // 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' ) ); @@ -619,36 +629,27 @@ class ResourceLoader { } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); wfDebugLog( 'resourceloader', __METHOD__ . ": calculating maximum modified time failed: $e" ); - $this->hasErrors = true; - // Add exception to the output as a comment - $errors .= self::formatException( $e ); + $this->errors[] = self::formatExceptionNoComment( $e ); } } - wfProfileOut( __METHOD__ . '-getModifiedTime' ); - // If there's an If-Modified-Since header, respond with a 304 appropriately if ( $this->tryRespondLastModified( $context, $mtime ) ) { - wfProfileOut( __METHOD__ ); return; // output handled (buffers cleared) } // Generate a response $response = $this->makeModuleResponse( $context, $modules, $missing ); - // Prepend comments indicating exceptions - $response = $errors . $response; - // Capture any PHP warnings from the output buffer and append them to the - // response in a comment if we're in debug mode. + // error list if we're in debug mode. if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) { - $response = self::makeComment( $warnings ) . $response; - $this->hasErrors = true; + $this->errors[] = $warnings; } // Save response to file cache unless there are errors - if ( isset( $fileCache ) && !$errors && !count( $missing ) ) { - // Cache single modules...and other requests if there are enough hits + if ( isset( $fileCache ) && !$this->errors && !count( $missing ) ) { + // Cache single modules and images...and other requests if there are enough hits if ( ResourceFileCache::useFileCache( $context ) ) { if ( $fileCache->isCacheWorthy() ) { $fileCache->saveText( $response ); @@ -659,20 +660,37 @@ class ResourceLoader { } // Send content type and cache related headers - $this->sendResponseHeaders( $context, $mtime, $this->hasErrors ); + $this->sendResponseHeaders( $context, $mtime, (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; + } elseif ( $this->errors ) { + // Prepend comments indicating errors + $errorText = ''; + foreach ( $this->errors as $error ) { + $errorText .= self::makeComment( $error ); + } + $response = $errorText . $response; + } + + $this->errors = array(); echo $response; - wfProfileOut( __METHOD__ ); } /** * Send content type and last modified headers to the client. * @param ResourceLoaderContext $context * @param string $mtime TS_MW timestamp to use for last-modified - * @param bool $errors Whether there are commented-out errors in the response + * @param bool $errors Whether there are errors in the response * @return void */ protected function sendResponseHeaders( ResourceLoaderContext $context, $mtime, $errors ) { @@ -689,7 +707,14 @@ class ResourceLoader { $maxage = $rlMaxage['versioned']['client']; $smaxage = $rlMaxage['versioned']['server']; } - if ( $context->getOnly() === 'styles' ) { + if ( $context->getImageObj() ) { + // Output different headers if we're outputting textual errors. + if ( $errors ) { + header( 'Content-Type: text/plain; charset=utf-8' ); + } else { + $context->getImageObj()->sendResponseHeaders( $context ); + } + } elseif ( $context->getOnly() === 'styles' ) { header( 'Content-Type: text/css; charset=utf-8' ); header( 'Access-Control-Allow-Origin: *' ); } else { @@ -813,15 +838,26 @@ class ResourceLoader { * Handle exception display. * * @param Exception $e Exception to be shown to the user - * @return string Sanitized text that can be returned to the user + * @return string Sanitized text in a CSS/JS comment that can be returned to the user */ public static function formatException( $e ) { + return self::makeComment( self::formatExceptionNoComment( $e ) ); + } + + /** + * Handle exception display. + * + * @since 1.25 + * @param Exception $e Exception to be shown to the user + * @return string Sanitized text that can be returned to the user + */ + protected static function formatExceptionNoComment( $e ) { global $wgShowExceptionDetails; if ( $wgShowExceptionDetails ) { - return self::makeComment( $e->__toString() ); + return $e->__toString(); } else { - return self::makeComment( wfMessage( 'internalerror' )->text() ); + return wfMessage( 'internalerror' )->text(); } } @@ -837,30 +873,37 @@ class ResourceLoader { array $modules, array $missing = array() ) { $out = ''; - $exceptions = ''; $states = array(); if ( !count( $modules ) && !count( $missing ) ) { - return "/* This file is the Web entry point for MediaWiki's ResourceLoader: + return <<<MESSAGE +/* This file is the Web entry point for MediaWiki's ResourceLoader: <https://www.mediawiki.org/wiki/ResourceLoader>. In this request, - no modules were requested. Max made me put this here. */"; + no modules were requested. Max made me put this here. */ +MESSAGE; } - wfProfileIn( __METHOD__ ); + $image = $context->getImageObj(); + if ( $image ) { + $data = $image->getImageData( $context ); + if ( $data === false ) { + $data = ''; + $this->errors[] = 'Image generation failed'; + } + return $data; + } // Pre-fetch blobs if ( $context->shouldIncludeMessages() ) { try { - $blobs = MessageBlobStore::getInstance()->get( $this, $modules, $context->getLanguage() ); + $blobs = $this->blobStore->get( $this, $modules, $context->getLanguage() ); } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); wfDebugLog( 'resourceloader', __METHOD__ . ": pre-fetching blobs from MessageBlobStore failed: $e" ); - $this->hasErrors = true; - // Add exception to the output as a comment - $exceptions .= self::formatException( $e ); + $this->errors[] = self::formatExceptionNoComment( $e ); } } else { $blobs = array(); @@ -877,7 +920,6 @@ class ResourceLoader { * @var $module ResourceLoaderModule */ - wfProfileIn( __METHOD__ . '-' . $name ); try { $scripts = ''; if ( $context->shouldIncludeScripts() ) { @@ -964,28 +1006,33 @@ class ResourceLoader { 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 ) + new XmlJsCode( $messagesBlob ), + $module->getTemplates() ); break; } } catch ( Exception $e ) { MWExceptionHandler::logException( $e ); wfDebugLog( 'resourceloader', __METHOD__ . ": generating module package failed: $e" ); - $this->hasErrors = true; - // Add exception to the output as a comment - $exceptions .= self::formatException( $e ); + $this->errors[] = self::formatExceptionNoComment( $e ); // Respond to client with error-state instead of module implementation $states[$name] = 'error'; unset( $modules[$name] ); } $isRaw |= $module->isRaw(); - wfProfileOut( __METHOD__ . '-' . $name ); } // Update module states @@ -1004,9 +1051,8 @@ class ResourceLoader { } } else { if ( count( $states ) ) { - $exceptions .= self::makeComment( - 'Problematic modules: ' . FormatJson::encode( $states, ResourceLoader::inDebugMode() ) - ); + $this->errors[] = 'Problematic modules: ' . + FormatJson::encode( $states, ResourceLoader::inDebugMode() ); } } @@ -1018,8 +1064,7 @@ class ResourceLoader { } } - wfProfileOut( __METHOD__ ); - return $exceptions . $out; + return $out; } /* Static Methods */ @@ -1034,30 +1079,32 @@ class ResourceLoader { * @param mixed $messages List of messages associated with this module. May either be an * associative array mapping message key to value, or a JSON-encoded message blob containing * the same data, wrapped in an XmlJsCode object. + * @param array $templates Keys are name of templates and values are the source of + * the template. * @throws MWException * @return string */ - public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { + public static function makeLoaderImplementScript( $name, $scripts, $styles, + $messages, $templates + ) { if ( is_string( $scripts ) ) { $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.' ); } - return Xml::encodeJsCall( - 'mw.loader.implement', - array( - $name, - $scripts, - // Force objects. mw.loader.implement requires them to be javascript objects. - // Although these variables are associative arrays, which become javascript - // objects through json_encode. In many cases they will be empty arrays, and - // PHP/json_encode() consider empty arrays to be numerical arrays and - // output javascript "[]" instead of "{}". This fixes that. - (object)$styles, - (object)$messages - ), - ResourceLoader::inDebugMode() + // mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not + // arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead + // of "{}". Force them to objects. + $module = array( + $name, + $scripts, + (object) $styles, + (object) $messages, + (object) $templates, ); + self::trimArray( $module ); + + return Xml::encodeJsCall( 'mw.loader.implement', $module, ResourceLoader::inDebugMode() ); } /** @@ -1164,6 +1211,40 @@ class ResourceLoader { ); } + private static function isEmptyObject( stdClass $obj ) { + foreach ( $obj as $key => &$value ) { + return false; + } + return true; + } + + /** + * Remove empty values from the end of an array. + * + * Values considered empty: + * + * - null + * - array() + * - new XmlJsCode( '{}' ) + * - new stdClass() // (object) array() + * + * @param Array $array + */ + private static function trimArray( Array &$array ) { + $i = count( $array ); + while ( $i-- ) { + if ( $array[$i] === null + || $array[$i] === array() + || ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' ) + || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) ) + ) { + unset( $array[$i] ); + } else { + break; + } + } + } + /** * Returns JS code which calls mw.loader.register with the given * parameters. Has three calling conventions: @@ -1195,16 +1276,37 @@ class ResourceLoader { $dependencies = null, $group = null, $source = null, $skip = null ) { if ( is_array( $name ) ) { + // Build module name index + $index = array(); + foreach ( $name as $i => &$module ) { + $index[$module[0]] = $i; + } + + // Transform dependency names into indexes when possible, they will be resolved by + // mw.loader.register on the other end + foreach ( $name as &$module ) { + if ( isset( $module[2] ) ) { + foreach ( $module[2] as &$dependency ) { + if ( isset( $index[$dependency] ) ) { + $dependency = $index[$dependency]; + } + } + } + } + + array_walk( $name, array( 'self', 'trimArray' ) ); + return Xml::encodeJsCall( 'mw.loader.register', array( $name ), ResourceLoader::inDebugMode() ); } else { - $version = (int)$version > 1 ? (int)$version : 1; + $registration = array( $name, $version, $dependencies, $group, $source, $skip ); + self::trimArray( $registration ); return Xml::encodeJsCall( 'mw.loader.register', - array( $name, $version, $dependencies, $group, $source, $skip ), + $registration, ResourceLoader::inDebugMode() ); } @@ -1466,6 +1568,9 @@ class ResourceLoader { // 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' ); } @@ -1488,9 +1593,13 @@ class ResourceLoader { * @return array Map of variable names to string CSS values. */ public static function getLessVars( Config $config ) { - $lessVars = $config->get( 'ResourceLoaderLESSVars' ); - // Sort by key to ensure consistent hashing for cache lookups. - ksort( $lessVars ); - return $lessVars; + if ( !self::$lessVars ) { + $lessVars = $config->get( 'ResourceLoaderLESSVars' ); + Hooks::run( 'ResourceLoaderGetLessVars', array( &$lessVars ) ); + // Sort by key to ensure consistent hashing for cache lookups. + ksort( $lessVars ); + self::$lessVars = $lessVars; + } + return self::$lessVars; } } diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index 7af7b898..a6a7d347 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -41,6 +41,11 @@ class ResourceLoaderContext { protected $version; protected $hash; protected $raw; + protected $image; + protected $variant; + protected $format; + protected $userObj; + protected $imageObj; /* Methods */ @@ -65,6 +70,10 @@ class ResourceLoaderContext { $this->only = $request->getVal( 'only' ); $this->version = $request->getVal( 'version' ); $this->raw = $request->getFuzzyBool( 'raw' ); + // Image requests + $this->image = $request->getVal( 'image' ); + $this->variant = $request->getVal( 'variant' ); + $this->format = $request->getVal( 'format' ); $skinnames = Skin::getSkinNames(); // If no skin is specified, or we don't recognize the skin, use the default skin @@ -179,6 +188,31 @@ class ResourceLoaderContext { } /** + * Get the possibly-cached User object for the specified username + * + * @since 1.25 + * @return User|bool false if a valid object cannot be created + */ + public function getUserObj() { + if ( $this->userObj === null ) { + $username = $this->getUser(); + if ( $username ) { + // Optimize: Avoid loading a new User object if possible + global $wgUser; + if ( is_object( $wgUser ) && $wgUser->getName() === $username ) { + $this->userObj = $wgUser; + } else { + $this->userObj = User::newFromName( $username ); + } + } else { + $this->userObj = new User; // Anonymous user + } + } + + return $this->userObj; + } + + /** * @return bool */ public function getDebug() { @@ -207,6 +241,62 @@ class ResourceLoaderContext { } /** + * @return string|null + */ + public function getImage() { + return $this->image; + } + + /** + * @return string|null + */ + public function getVariant() { + return $this->variant; + } + + /** + * @return string|null + */ + public function getFormat() { + return $this->format; + } + + /** + * If this is a request for an image, get the ResourceLoaderImage object. + * + * @since 1.25 + * @return ResourceLoaderImage|bool false if a valid object cannot be created + */ + public function getImageObj() { + if ( $this->imageObj === null ) { + $this->imageObj = false; + + if ( !$this->image ) { + return $this->imageObj; + } + + $modules = $this->getModules(); + if ( count( $modules ) !== 1 ) { + return $this->imageObj; + } + + $module = $this->getResourceLoader()->getModule( $modules[0] ); + if ( !$module || !$module instanceof ResourceLoaderImageModule ) { + return $this->imageObj; + } + + $image = $module->getImage( $this->image ); + if ( !$image ) { + return $this->imageObj; + } + + $this->imageObj = $image; + } + + return $this->imageObj; + } + + /** * @return bool */ public function shouldIncludeScripts() { @@ -234,6 +324,7 @@ class ResourceLoaderContext { if ( !isset( $this->hash ) ) { $this->hash = implode( '|', array( $this->getLanguage(), $this->getDirection(), $this->getSkin(), $this->getUser(), + $this->getImage(), $this->getVariant(), $this->getFormat(), $this->getDebug(), $this->getOnly(), $this->getVersion() ) ); } diff --git a/includes/resourceloader/ResourceLoaderEditToolbarModule.php b/includes/resourceloader/ResourceLoaderEditToolbarModule.php index 2e07911c..d79174cd 100644 --- a/includes/resourceloader/ResourceLoaderEditToolbarModule.php +++ b/includes/resourceloader/ResourceLoaderEditToolbarModule.php @@ -32,6 +32,7 @@ class ResourceLoaderEditToolbarModule extends ResourceLoaderFileModule { * * @param string $value * @return string + * @throws Exception */ private static function cssSerializeString( $value ) { if ( strstr( $value, "\0" ) ) { diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index dc8b14a2..671098e1 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -34,6 +34,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** @var string Remote base path, see __construct() */ protected $remoteBasePath = ''; + /** @var array Saves a list of the templates named by the modules. */ + protected $templates = array(); + /** * @var array List of paths to JavaScript files to always include * @par Usage: @@ -171,7 +174,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * to $wgResourceBasePath * * Below is a description for the $options array: - * @throws MWException + * @throws InvalidArgumentException * @par Construction options: * @code * array( @@ -199,6 +202,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * 'loaderScripts' => [file path string or array of file path strings], * // Modules which must be loaded before this module * 'dependencies' => [module name string or array of module name strings], + * 'templates' => array( + * [template alias with file.ext] => [file path to a template file], + * ), * // Styles to always load * 'styles' => [file path string or array of file path strings], * // Styles to include in specific skin contexts @@ -223,6 +229,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $localBasePath = null, $remoteBasePath = null ) { + // Flag to decide whether to automagically add the mediawiki.template module + $hasTemplates = false; // localBasePath and remoteBasePath both have unbelievably long fallback chains // and need to be handled separately. list( $this->localBasePath, $this->remoteBasePath ) = @@ -238,19 +246,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { case 'styles': $this->{$member} = (array)$option; break; + case 'templates': + $hasTemplates = true; + $this->{$member} = (array)$option; + break; // Collated lists of file paths case 'languageScripts': case 'skinScripts': case 'skinStyles': if ( !is_array( $option ) ) { - throw new MWException( + throw new InvalidArgumentException( "Invalid collated file path list error. " . "'$option' given, array expected." ); } foreach ( $option as $key => $value ) { if ( !is_string( $key ) ) { - throw new MWException( + throw new InvalidArgumentException( "Invalid collated file path list key error. " . "'$key' given, string expected." ); @@ -281,6 +293,21 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { break; } } + if ( $hasTemplates ) { + $this->dependencies[] = 'mediawiki.template'; + // Ensure relevant template compiler module gets loaded + foreach ( $this->templates as $alias => $templatePath ) { + if ( is_int( $alias ) ) { + $alias = $templatePath; + } + $suffix = explode( '.', $alias ); + $suffix = end( $suffix ); + $compilerModule = 'mediawiki.template.' . $suffix; + if ( $suffix !== 'html' && !in_array( $compilerModule, $this->dependencies ) ) { + $this->dependencies[] = $compilerModule; + } + } + } } /** @@ -304,7 +331,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { // The different ways these checks are done, and their ordering, look very silly, // but were preserved for backwards-compatibility just in case. Tread lightly. - $localBasePath = $localBasePath === null ? $IP : $localBasePath; + if ( $localBasePath === null ) { + $localBasePath = $IP; + } if ( $remoteBasePath === null ) { $remoteBasePath = $wgResourceBasePath; } @@ -466,8 +495,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Get the skip function. - * - * @return string|null + * @return null|string + * @throws MWException */ public function getSkipFunction() { if ( !$this->skipFunction ) { @@ -510,7 +539,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { if ( isset( $this->modifiedTime[$context->getHash()] ) ) { return $this->modifiedTime[$context->getHash()]; } - wfProfileIn( __METHOD__ ); $files = array(); @@ -533,8 +561,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $files = array_merge( $files, $this->scripts, + $this->templates, $context->getDebug() ? $this->debugScripts : array(), - self::tryForKey( $this->languageScripts, $context->getLanguage() ), + $this->getLanguageScripts( $context->getLanguage() ), self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ), $this->loaderScripts ); @@ -544,18 +573,19 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $files = array_map( array( $this, 'getLocalPath' ), $files ); // File deps need to be treated separately because they're already prefixed $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) ); + // Filter out any duplicates from getFileDependencies() and others. + // Most commonly introduced by compileLessFile(), which always includes the + // 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; - wfProfileOut( __METHOD__ ); return $this->modifiedTime[$context->getHash()]; } - wfProfileIn( __METHOD__ . '-filemtime' ); $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); - wfProfileOut( __METHOD__ . '-filemtime' ); $this->modifiedTime[$context->getHash()] = max( $filesMtime, @@ -563,7 +593,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $this->getDefinitionMtime( $context ) ); - wfProfileOut( __METHOD__ ); return $this->modifiedTime[$context->getHash()]; } @@ -574,9 +603,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * @return array */ public function getDefinitionSummary( ResourceLoaderContext $context ) { - $summary = array( - 'class' => get_class( $this ), - ); + $summary = parent::getDefinitionSummary( $context ); foreach ( array( 'scripts', 'debugScripts', @@ -588,6 +615,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { 'dependencies', 'messages', 'targets', + 'templates', 'group', 'position', 'skipFunction', @@ -698,7 +726,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected function getScriptFiles( ResourceLoaderContext $context ) { $files = array_merge( $this->scripts, - self::tryForKey( $this->languageScripts, $context->getLanguage() ), + $this->getLanguageScripts( $context->getLanguage() ), self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) ); if ( $context->getDebug() ) { @@ -709,6 +737,29 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } /** + * Get the set of language scripts for the given language, + * possibly using a fallback language. + * + * @param string $lang + * @return array + */ + private function getLanguageScripts( $lang ) { + $scripts = self::tryForKey( $this->languageScripts, $lang ); + if ( $scripts ) { + return $scripts; + } + $fallbacks = Language::getFallbacksFor( $lang ); + foreach ( $fallbacks as $lang ) { + $scripts = self::tryForKey( $this->languageScripts, $lang ); + if ( $scripts ) { + return $scripts; + } + } + + return array(); + } + + /** * Get a list of file paths for all styles in this module, in order of proper inclusion. * * @param ResourceLoaderContext $context @@ -934,4 +985,30 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected function getLessCompiler( ResourceLoaderContext $context = null ) { return ResourceLoader::getLessCompiler( $this->getConfig() ); } + + /** + * Takes named templates by the module and returns an array mapping. + * @return array of templates mapping template alias to content + * @throws MWException + */ + public function getTemplates() { + $templates = array(); + + foreach ( $this->templates as $alias => $templatePath ) { + // Alias is optional + if ( is_int( $alias ) ) { + $alias = $templatePath; + } + $localPath = $this->getLocalPath( $templatePath ); + if ( file_exists( $localPath ) ) { + $content = file_get_contents( $localPath ); + $templates[$alias] = $content; + } else { + $msg = __METHOD__ . ": template file not found: \"$localPath\""; + wfDebugLog( 'resourceloader', $msg ); + throw new MWException( $msg ); + } + } + return $templates; + } } diff --git a/includes/resourceloader/ResourceLoaderFilePageModule.php b/includes/resourceloader/ResourceLoaderFilePageModule.php deleted file mode 100644 index 8c7fbe76..00000000 --- a/includes/resourceloader/ResourceLoaderFilePageModule.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php -/** - * Resource loader module for MediaWiki:Filepage.css - * - * 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 definition for MediaWiki:Filepage.css - */ -class ResourceLoaderFilePageModule extends ResourceLoaderWikiModule { - - /** - * @param ResourceLoaderContext $context - * @return array - */ - protected function getPages( ResourceLoaderContext $context ) { - return array( - 'MediaWiki:Filepage.css' => array( 'type' => 'style' ), - ); - } -} diff --git a/includes/resourceloader/ResourceLoaderImage.php b/includes/resourceloader/ResourceLoaderImage.php new file mode 100644 index 00000000..12d1e827 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderImage.php @@ -0,0 +1,388 @@ +<?php +/** + * Class encapsulating an image used in a ResourceLoaderImageModule. + * + * 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 + */ + +/** + * Class encapsulating an image used in a ResourceLoaderImageModule. + * + * @since 1.25 + */ +class ResourceLoaderImage { + + /** + * Map of allowed file extensions to their MIME types. + * @var array + */ + protected static $fileTypes = array( + 'svg' => 'image/svg+xml', + 'png' => 'image/png', + 'gif' => 'image/gif', + 'jpg' => 'image/jpg', + ); + + /** + * @param string $name Image name + * @param string $module Module name + * @param string|array $descriptor Path to image file, or array structure containing paths + * @param string $basePath Directory to which paths in descriptor refer + * @param array $variants + * @throws InvalidArgumentException + */ + public function __construct( $name, $module, $descriptor, $basePath, $variants ) { + $this->name = $name; + $this->module = $module; + $this->descriptor = $descriptor; + $this->basePath = $basePath; + $this->variants = $variants; + + // Expand shorthands: + // 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 ] + ); + unset( $this->descriptor['lang'][ $langList ] ); + } + } + } + + // Ensure that all files have common extension. + $extensions = array(); + $descriptor = (array)$descriptor; + array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) { + $extensions[] = pathinfo( $path, PATHINFO_EXTENSION ); + } ); + $extensions = array_unique( $extensions ); + if ( count( $extensions ) !== 1 ) { + 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)" ); + } + $this->extension = $ext; + } + + /** + * Get name of this image. + * + * @return string + */ + public function getName() { + return $this->name; + } + + /** + * Get name of the module this image belongs to. + * + * @return string + */ + public function getModule() { + return $this->module; + } + + /** + * Get the list of variants this image can be converted to. + * + * @return string[] + */ + public function getVariants() { + return array_keys( $this->variants ); + } + + /** + * Get the path to image file for given context. + * + * @param ResourceLoaderContext $context Any context + * @return string + */ + protected 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() ]; + } else { + return $this->basePath . '/' . $desc['default']; + } + } + + /** + * Get the extension of the image. + * + * @param string $format Format to get the extension for, 'original' or 'rasterized' + * @return string Extension without leading dot, e.g. 'png' + */ + public function getExtension( $format = 'original' ) { + if ( $format === 'rasterized' && $this->extension === 'svg' ) { + return 'png'; + } else { + return $this->extension; + } + } + + /** + * Get the MIME type of the image. + * + * @param string $format Format to get the MIME type for, 'original' or 'rasterized' + * @return string + */ + public function getMimeType( $format = 'original' ) { + $ext = $this->getExtension( $format ); + return self::$fileTypes[$ext]; + } + + /** + * Get the load.php URL that will produce this image. + * + * @param ResourceLoaderContext $context Any context + * @param string $script URL to load.php + * @param string|null $variant Variant to get the URL for + * @param string $format Format to get the URL for, 'original' or 'rasterized' + * @return string + */ + public function getUrl( ResourceLoaderContext $context, $script, $variant, $format ) { + $query = array( + 'modules' => $this->getModule(), + 'image' => $this->getName(), + 'variant' => $variant, + 'format' => $format, + 'lang' => $context->getLanguage(), + 'version' => $context->getVersion(), + ); + + return wfExpandUrl( wfAppendQuery( $script, $query ), PROTO_RELATIVE ); + } + + /** + * Get the data: URI that will produce this image. + * + * @param ResourceLoaderContext $context Any context + * @param string|null $variant Variant to get the URI for + * @param string $format Format to get the URI for, 'original' or 'rasterized' + * @return string + */ + public function getDataUri( ResourceLoaderContext $context, $variant, $format ) { + $type = $this->getMimeType( $format ); + $contents = $this->getImageData( $context, $variant, $format ); + return CSSMin::encodeStringAsDataURI( $contents, $type ); + } + + /** + * Get actual image data for this image. This can be saved to a file or sent to the browser to + * produce the converted image. + * + * Call getExtension() or getMimeType() with the same $format argument to learn what file type the + * returned data uses. + * + * @param ResourceLoaderContext $context Image context, or any context if $variant and $format + * given. + * @param string|null $variant Variant to get the data for. Optional; if given, overrides the data + * from $context. + * @param string $format Format to get the data for, 'original' or 'rasterized'. Optional; if + * given, overrides the data from $context. + * @return string|false Possibly binary image data, or false on failure + * @throws MWException If the image file doesn't exist + */ + public function getImageData( ResourceLoaderContext $context, $variant = false, $format = false ) { + if ( $variant === false ) { + $variant = $context->getVariant(); + } + if ( $format === false ) { + $format = $context->getFormat(); + } + + $path = $this->getPath( $context ); + if ( !file_exists( $path ) ) { + throw new MWException( "File '$path' does not exist" ); + } + + if ( $this->getExtension() !== 'svg' ) { + return file_get_contents( $path ); + } + + if ( $variant && isset( $this->variants[$variant] ) ) { + $data = $this->variantize( $this->variants[$variant], $context ); + } else { + $data = file_get_contents( $path ); + } + + if ( $format === 'rasterized' ) { + $data = $this->rasterize( $data ); + if ( !$data ) { + wfDebugLog( 'ResourceLoaderImage', __METHOD__ . " failed to rasterize for $path" ); + } + } + + return $data; + } + + /** + * Send response headers (using the header() function) that are necessary to correctly serve the + * image data for this image, as returned by getImageData(). + * + * Note that the headers are independent of the language or image variant. + * + * @param ResourceLoaderContext $context Image context + */ + public function sendResponseHeaders( ResourceLoaderContext $context ) { + $format = $context->getFormat(); + $mime = $this->getMimeType( $format ); + $filename = $this->getName() . '.' . $this->getExtension( $format ); + + header( 'Content-Type: ' . $mime ); + header( 'Content-Disposition: ' . + FileBackend::makeContentDisposition( 'inline', $filename ) ); + } + + /** + * Convert this image, which is assumed to be SVG, to given variant. + * + * @param array $variantConf Array with a 'color' key, its value will be used as fill color + * @param ResourceLoaderContext $context Image context + * @return string New SVG file data + */ + protected function variantize( $variantConf, ResourceLoaderContext $context ) { + $dom = new DomDocument; + $dom->load( $this->getPath( $context ) ); + $root = $dom->documentElement; + $wrapper = $dom->createElement( 'g' ); + while ( $root->firstChild ) { + $wrapper->appendChild( $root->firstChild ); + } + $root->appendChild( $wrapper ); + $wrapper->setAttribute( 'fill', $variantConf['color'] ); + return $dom->saveXml(); + } + + /** + * Massage the SVG image data for converters which don't understand some path data syntax. + * + * This is necessary for rsvg and ImageMagick when compiled with rsvg support. + * Upstream bug is https://bugzilla.gnome.org/show_bug.cgi?id=620923, fixed 2014-11-10, so + * this will be needed for a while. (T76852) + * + * @param string $svg SVG image data + * @return string Massaged SVG image data + */ + protected function massageSvgPathdata( $svg ) { + $dom = new DomDocument; + $dom->loadXml( $svg ); + foreach ( $dom->getElementsByTagName( 'path' ) as $node ) { + $pathData = $node->getAttribute( 'd' ); + // Make sure there is at least one space between numbers, and that leading zero is not omitted. + // rsvg has issues with syntax like "M-1-2" and "M.445.483" and especially "M-.445-.483". + $pathData = preg_replace( '/(-?)(\d*\.\d+|\d+)/', ' ${1}0$2 ', $pathData ); + // Strip unnecessary leading zeroes for prettiness, not strictly necessary + $pathData = preg_replace( '/([ -])0(\d)/', '$1$2', $pathData ); + $node->setAttribute( 'd', $pathData ); + } + return $dom->saveXml(); + } + + /** + * Convert passed image data, which is assumed to be SVG, to PNG. + * + * @param string $svg SVG image data + * @return string|bool PNG image data, or false on failure + */ + protected function rasterize( $svg ) { + // This code should be factored out to a separate method on SvgHandler, or perhaps a separate + // class, with a separate set of configuration settings. + // + // This is a distinct use case from regular SVG rasterization: + // * We can skip many sanity and security checks (as the images come from a trusted source, + // rather than from the user). + // * We need to provide extra options to some converters to achieve acceptable quality for very + // small images, which might cause performance issues in the general case. + // * We want to directly pass image data to the converter, rather than a file path. + // + // See https://phabricator.wikimedia.org/T76473#801446 for examples of what happens with the + // default settings. + // + // For now, we special-case rsvg (used in WMF production) and do a messy workaround for other + // converters. + + global $wgSVGConverter, $wgSVGConverterPath; + + $svg = $this->massageSvgPathdata( $svg ); + + // Sometimes this might be 'rsvg-secure'. Long as it's rsvg. + if ( strpos( $wgSVGConverter, 'rsvg' ) === 0 ) { + $command = 'rsvg-convert'; + if ( $wgSVGConverterPath ) { + $command = wfEscapeShellArg( "$wgSVGConverterPath/" ) . $command; + } + + $process = proc_open( + $command, + array( 0 => array( 'pipe', 'r' ), 1 => array( 'pipe', 'w' ) ), + $pipes + ); + + if ( is_resource( $process ) ) { + fwrite( $pipes[0], $svg ); + fclose( $pipes[0] ); + $png = stream_get_contents( $pipes[1] ); + fclose( $pipes[1] ); + proc_close( $process ); + + return $png ?: false; + } + return false; + + } else { + // Write input to and read output from a temporary file + $tempFilenameSvg = tempnam( wfTempDir(), 'ResourceLoaderImage' ); + $tempFilenamePng = tempnam( wfTempDir(), 'ResourceLoaderImage' ); + + file_put_contents( $tempFilenameSvg, $svg ); + + $metadata = SVGMetadataExtractor::getMetadata( $tempFilenameSvg ); + if ( !isset( $metadata['width'] ) || !isset( $metadata['height'] ) ) { + unlink( $tempFilenameSvg ); + return false; + } + + $handler = new SvgHandler; + $res = $handler->rasterize( + $tempFilenameSvg, + $tempFilenamePng, + $metadata['width'], + $metadata['height'] + ); + unlink( $tempFilenameSvg ); + + $png = null; + if ( $res === true ) { + $png = file_get_contents( $tempFilenamePng ); + unlink( $tempFilenamePng ); + } + + return $png ?: false; + } + } +} diff --git a/includes/resourceloader/ResourceLoaderImageModule.php b/includes/resourceloader/ResourceLoaderImageModule.php new file mode 100644 index 00000000..bf6a7dd2 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderImageModule.php @@ -0,0 +1,327 @@ +<?php +/** + * Resource loader module for generated and embedded images. + * + * 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 Trevor Parscal + */ + +/** + * Resource loader module for generated and embedded images. + * + * @since 1.25 + */ +class ResourceLoaderImageModule extends ResourceLoaderModule { + + /** + * Local base path, see __construct() + * @var string + */ + protected $localBasePath = ''; + + protected $origin = self::ORIGIN_CORE_SITEWIDE; + + protected $images = array(); + protected $variants = array(); + protected $prefix = null; + protected $selectorWithoutVariant = '.{prefix}-{name}'; + protected $selectorWithVariant = '.{prefix}-{name}-{variant}'; + protected $targets = array( 'desktop', 'mobile' ); + + /** + * Constructs a new module from an options array. + * + * @param array $options List of options; if not given or empty, an empty module will be + * constructed + * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults + * to $IP + * + * Below is a description for the $options array: + * @par Construction options: + * @code + * array( + * // Base path to prepend to all local paths in $options. Defaults to $IP + * 'localBasePath' => [base path], + * // CSS class prefix to use in all style rules + * 'prefix' => [CSS class prefix], + * // Alternatively: Format of CSS selector to use in all style rules + * 'selector' => [CSS selector template, variables: {prefix} {name} {variant}], + * // Alternatively: When using variants + * 'selectorWithoutVariant' => [CSS selector template, variables: {prefix} {name}], + * 'selectorWithVariant' => [CSS selector template, variables: {prefix} {name} {variant}], + * // List of variants that may be used for the image files + * 'variants' => 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], + * 'variants' => [array of variant name strings, variants + * available for this image], + * ), + * ... + * ), + * ) + * @endcode + * @throws InvalidArgumentException + */ + public function __construct( $options = array(), $localBasePath = null ) { + $this->localBasePath = self::extractLocalBasePath( $options, $localBasePath ); + + // Accepted combinations: + // * prefix + // * selector + // * selectorWithoutVariant + selectorWithVariant + // * prefix + selector + // * prefix + selectorWithoutVariant + selectorWithVariant + + $prefix = isset( $options['prefix'] ) && $options['prefix']; + $selector = isset( $options['selector'] ) && $options['selector']; + $selectorWithoutVariant = isset( $options['selectorWithoutVariant'] ) && $options['selectorWithoutVariant']; + $selectorWithVariant = isset( $options['selectorWithVariant'] ) && $options['selectorWithVariant']; + + if ( $selectorWithoutVariant && !$selectorWithVariant ) { + throw new InvalidArgumentException( "Given 'selectorWithoutVariant' but no 'selectorWithVariant'." ); + } + if ( $selectorWithVariant && !$selectorWithoutVariant ) { + throw new InvalidArgumentException( "Given 'selectorWithVariant' but no 'selectorWithoutVariant'." ); + } + if ( $selector && $selectorWithVariant ) { + throw new InvalidArgumentException( "Incompatible 'selector' and 'selectorWithVariant'+'selectorWithoutVariant' given." ); + } + if ( !$prefix && !$selector && !$selectorWithVariant ) { + throw new InvalidArgumentException( "None of 'prefix', 'selector' or 'selectorWithVariant'+'selectorWithoutVariant' given." ); + } + + foreach ( $options as $member => $option ) { + switch ( $member ) { + case 'images': + case 'variants': + if ( !is_array( $option ) ) { + throw new InvalidArgumentException( + "Invalid list error. '$option' given, array expected." + ); + } + $this->{$member} = $option; + break; + + case 'prefix': + case 'selectorWithoutVariant': + case 'selectorWithVariant': + $this->{$member} = (string)$option; + break; + + case 'selector': + $this->selectorWithoutVariant = $this->selectorWithVariant = (string)$option; + } + } + } + + /** + * Get CSS class prefix used by this module. + * @return string + */ + public function getPrefix() { + return $this->prefix; + } + + /** + * Get CSS selector templates used by this module. + * @return string + */ + public function getSelectors() { + return array( + 'selectorWithoutVariant' => $this->selectorWithoutVariant, + 'selectorWithVariant' => $this->selectorWithVariant, + ); + } + + /** + * Get a ResourceLoaderImage object for given image. + * @param string $name Image name + * @return ResourceLoaderImage|null + */ + public function getImage( $name ) { + $images = $this->getImages(); + return isset( $images[$name] ) ? $images[$name] : null; + } + + /** + * Get ResourceLoaderImage objects for all images. + * @return ResourceLoaderImage[] Array keyed by image name + */ + public function getImages() { + if ( !isset( $this->imageObjects ) ) { + $this->imageObjects = array(); + + foreach ( $this->images as $name => $options ) { + $fileDescriptor = is_string( $options ) ? $options : $options['file']; + + $allowedVariants = array_merge( + is_array( $options ) && isset( $options['variants'] ) ? $options['variants'] : array(), + $this->getGlobalVariants() + ); + if ( isset( $this->variants ) ) { + $variantConfig = array_intersect_key( + $this->variants, + array_fill_keys( $allowedVariants, true ) + ); + } else { + $variantConfig = array(); + } + + $image = new ResourceLoaderImage( + $name, + $this->getName(), + $fileDescriptor, + $this->localBasePath, + $variantConfig + ); + $this->imageObjects[ $image->getName() ] = $image; + } + } + + return $this->imageObjects; + } + + /** + * Get list of variants in this module that are 'global', i.e., available + * for every image regardless of image options. + * @return string[] + */ + public function getGlobalVariants() { + if ( !isset( $this->globalVariants ) ) { + $this->globalVariants = array(); + + if ( isset( $this->variants ) ) { + foreach ( $this->variants as $name => $config ) { + if ( isset( $config['global'] ) && $config['global'] ) { + $this->globalVariants[] = $name; + } + } + } + } + + return $this->globalVariants; + } + + /** + * @param ResourceLoaderContext $context + * @return array + */ + public function getStyles( ResourceLoaderContext $context ) { + // Build CSS rules + $rules = array(); + $script = $context->getResourceLoader()->getLoadScript( $this->getSource() ); + $selectors = $this->getSelectors(); + + foreach ( $this->getImages() as $name => $image ) { + $declarations = $this->getCssDeclarations( + $image->getDataUri( $context, null, 'original' ), + $image->getUrl( $context, $script, null, 'rasterized' ) + ); + $declarations = implode( "\n\t", $declarations ); + $selector = strtr( + $selectors['selectorWithoutVariant'], + array( + '{prefix}' => $this->getPrefix(), + '{name}' => $name, + '{variant}' => '', + ) + ); + $rules[] = "$selector {\n\t$declarations\n}"; + + foreach ( $image->getVariants() as $variant ) { + $declarations = $this->getCssDeclarations( + $image->getDataUri( $context, $variant, 'original' ), + $image->getUrl( $context, $script, $variant, 'rasterized' ) + ); + $declarations = implode( "\n\t", $declarations ); + $selector = strtr( + $selectors['selectorWithVariant'], + array( + '{prefix}' => $this->getPrefix(), + '{name}' => $name, + '{variant}' => $variant, + ) + ); + $rules[] = "$selector {\n\t$declarations\n}"; + } + } + + $style = implode( "\n", $rules ); + return array( 'all' => $style ); + } + + /** + * SVG support using a transparent gradient to guarantee cross-browser + * compatibility (browsers able to understand gradient syntax support also SVG). + * http://pauginer.tumblr.com/post/36614680636/invisible-gradient-technique + * + * Keep synchronized with the .background-image-svg LESS mixin in + * /resources/src/mediawiki.less/mediawiki.mixins.less. + * + * @param string $primary Primary URI + * @param string $fallback Fallback URI + * @return string[] CSS declarations to use given URIs as background-image + */ + protected function getCssDeclarations( $primary, $fallback ) { + return array( + "background-image: url($fallback);", + "background-image: -webkit-linear-gradient(transparent, transparent), url($primary);", + "background-image: linear-gradient(transparent, transparent), url($primary);", + "background-image: -o-linear-gradient(transparent, transparent), url($fallback);", + ); + } + + /** + * @return bool + */ + public function supportsURLLoading() { + return false; + } + + /** + * Extract a local base path from module definition information. + * + * @param array $options Module definition + * @param string $localBasePath Path to use if not provided in module definition. Defaults + * to $IP + * @return string Local base path + */ + public static function extractLocalBasePath( $options, $localBasePath = null ) { + global $IP; + + if ( $localBasePath === null ) { + $localBasePath = $IP; + } + + if ( array_key_exists( 'localBasePath', $options ) ) { + $localBasePath = (string)$options['localBasePath']; + } + + return $localBasePath; + } +} diff --git a/includes/resourceloader/ResourceLoaderLanguageDataModule.php b/includes/resourceloader/ResourceLoaderLanguageDataModule.php index 09d90d6e..12394536 100644 --- a/includes/resourceloader/ResourceLoaderLanguageDataModule.php +++ b/includes/resourceloader/ResourceLoaderLanguageDataModule.php @@ -52,10 +52,14 @@ class ResourceLoaderLanguageDataModule extends ResourceLoaderModule { * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { - return Xml::encodeJsCall( 'mw.language.setData', array( - $context->getLanguage(), - $this->getData( $context ) - ) ); + return Xml::encodeJsCall( + 'mw.language.setData', + array( + $context->getLanguage(), + $this->getData( $context ) + ), + ResourceLoader::inDebugMode() + ); } /** diff --git a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php index fe0c8454..55b1f4b1 100644 --- a/includes/resourceloader/ResourceLoaderLanguageNamesModule.php +++ b/includes/resourceloader/ResourceLoaderLanguageNamesModule.php @@ -49,11 +49,15 @@ class ResourceLoaderLanguageNamesModule extends ResourceLoaderModule { * @return string JavaScript code */ public function getScript( ResourceLoaderContext $context ) { - return Xml::encodeJsCall( 'mw.language.setData', array( - $context->getLanguage(), - 'languageNames', - $this->getData( $context ) - ) ); + return Xml::encodeJsCall( + 'mw.language.setData', + array( + $context->getLanguage(), + 'languageNames', + $this->getData( $context ) + ), + ResourceLoader::inDebugMode() + ); } public function getDependencies() { diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index 45eb70f8..ed16521b 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -135,6 +135,16 @@ abstract class ResourceLoaderModule { } /** + * Takes named templates by the module and returns an array mapping. + * + * @return array of templates mapping template alias to content + */ + public function getTemplates() { + // Stub, override expected. + return array(); + } + + /** * @return Config * @since 1.24 */ @@ -378,12 +388,12 @@ abstract class ResourceLoaderModule { * Get the last modification timestamp of the message blob for this * module in a given language. * @param string $lang Language code - * @return int UNIX timestamp, or 0 if the module doesn't have messages + * @return int UNIX timestamp */ public function getMsgBlobMtime( $lang ) { if ( !isset( $this->msgBlobMtime[$lang] ) ) { if ( !count( $this->getMessages() ) ) { - return 0; + return 1; } $dbr = wfGetDB( DB_SLAVE ); @@ -406,7 +416,7 @@ abstract class ResourceLoaderModule { * Set a preloaded message blob last modification timestamp. Used so we * can load this information for all modules at once. * @param string $lang Language code - * @param int $mtime UNIX timestamp or 0 if there is no such blob + * @param int $mtime UNIX timestamp */ public function setMsgBlobMtime( $lang, $mtime ) { $this->msgBlobMtime[$lang] = $mtime; @@ -433,7 +443,6 @@ abstract class ResourceLoaderModule { * @return int UNIX timestamp */ public function getModifiedTime( ResourceLoaderContext $context ) { - // 0 would mean now return 1; } @@ -441,30 +450,34 @@ abstract class ResourceLoaderModule { * Helper method for calculating when the module's hash (if it has one) changed. * * @param ResourceLoaderContext $context - * @return int UNIX timestamp or 0 if no hash was provided - * by getModifiedHash() + * @return int UNIX timestamp */ public function getHashMtime( ResourceLoaderContext $context ) { $hash = $this->getModifiedHash( $context ); if ( !is_string( $hash ) ) { - return 0; + return 1; } + // 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', 'modulemodifiedhash', $this->getName(), $hash ); + $key = wfMemcKey( 'resourceloader', 'hashmtime', $this->getName(), $hash ); $data = $cache->get( $key ); - if ( is_array( $data ) && $data['hash'] === $hash ) { - // Hash is still the same, re-use the timestamp of when we first saw this hash. - return $data['timestamp']; + if ( is_int( $data ) && $data > 0 ) { + // We've seen this hash before, re-use the timestamp of when we first saw it. + return $data; } - $timestamp = wfTimestamp(); - $cache->set( $key, array( - 'hash' => $hash, - 'timestamp' => $timestamp, - ) ); - + $timestamp = time(); + $cache->set( $key, $timestamp ); return $timestamp; } @@ -487,46 +500,29 @@ abstract class ResourceLoaderModule { * @since 1.23 * * @param ResourceLoaderContext $context - * @return int UNIX timestamp or 0 if no definition summary was provided - * by getDefinitionSummary() + * @return int UNIX timestamp */ public function getDefinitionMtime( ResourceLoaderContext $context ) { - wfProfileIn( __METHOD__ ); $summary = $this->getDefinitionSummary( $context ); if ( $summary === null ) { - wfProfileOut( __METHOD__ ); - return 0; + return 1; } $hash = md5( json_encode( $summary ) ); - $cache = wfGetCache( CACHE_ANYTHING ); - - // 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. $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. - wfProfileOut( __METHOD__ ); return $data; } - wfDebugLog( 'resourceloader', __METHOD__ . ": New definition hash for module " - . "{$this->getName()} in context {$context->getHash()}: $hash." ); + wfDebugLog( 'resourceloader', __METHOD__ . ": New definition for module " + . "{$this->getName()} in context \"{$context->getHash()}\"" ); $timestamp = time(); $cache->set( $key, $timestamp ); - - wfProfileOut( __METHOD__ ); return $timestamp; } @@ -630,16 +626,13 @@ 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 - * @return int UNIX timestamp, or 1 if the file doesn't exist + * @return int UNIX timestamp */ protected static function safeFilemtime( $filename ) { - if ( file_exists( $filename ) ) { - return filemtime( $filename ); - } else { - // We only ever map this function on an array if we're gonna call max() after, - // so return our standard minimum timestamps here. This is 1, not 0, because - // wfTimestamp(0) == NOW - return 1; - } + wfSuppressWarnings(); + $mtime = filemtime( $filename ) ?: 1; + wfRestoreWarnings(); + + return $mtime; } } diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php index 1d9721aa..19e0baeb 100644 --- a/includes/resourceloader/ResourceLoaderSiteModule.php +++ b/includes/resourceloader/ResourceLoaderSiteModule.php @@ -27,13 +27,10 @@ */ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule { - /* Protected Methods */ - /** - * Gets list of pages used by this module + * Get list of pages used by this module * * @param ResourceLoaderContext $context - * * @return array List of pages */ protected function getPages( ResourceLoaderContext $context ) { @@ -45,18 +42,16 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule { if ( $this->getConfig()->get( 'UseSiteCss' ) ) { $pages['MediaWiki:Common.css'] = array( 'type' => 'style' ); $pages['MediaWiki:' . ucfirst( $context->getSkin() ) . '.css'] = array( 'type' => 'style' ); + $pages['MediaWiki:Print.css'] = array( 'type' => 'style', 'media' => 'print' ); } - $pages['MediaWiki:Print.css'] = array( 'type' => 'style', 'media' => 'print' ); return $pages; } - /* Methods */ - /** - * Gets group name + * Get group name * - * @return string Name of group + * @return string */ public function getGroup() { return 'site'; diff --git a/includes/resourceloader/ResourceLoaderSkinModule.php b/includes/resourceloader/ResourceLoaderSkinModule.php new file mode 100644 index 00000000..3ba63e68 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderSkinModule.php @@ -0,0 +1,92 @@ +<?php +/** + * Resource loader module for skin stylesheets. + * + * 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 ResourceLoaderSkinModule extends ResourceLoaderFileModule { + + /* Methods */ + + /** + * @param $context ResourceLoaderContext + * @return array + */ + public function getStyles( ResourceLoaderContext $context ) { + $logo = $this->getConfig()->get( 'Logo' ); + $logoHD = $this->getConfig()->get( 'LogoHD' ); + $styles = parent::getStyles( $context ); + $styles['all'][] = '.mw-wiki-logo { background-image: ' . + CSSMin::buildUrlValue( $logo ) . + '; }'; + if ( $logoHD ) { + if ( isset( $logoHD['1.5x'] ) ) { + $styles[ + '(-webkit-min-device-pixel-ratio: 1.5), ' . + '(min--moz-device-pixel-ratio: 1.5), ' . + '(min-resolution: 1.5dppx), ' . + '(min-resolution: 144dpi)' + ][] = '.mw-wiki-logo { background-image: ' . + 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-resolution: 2dppx), ' . + '(min-resolution: 192dpi)' + ][] = '.mw-wiki-logo { background-image: ' . + CSSMin::buildUrlValue( $logoHD['2x'] ) . ';' . + 'background-size: 135px auto; }'; + } + } + return $styles; + } + + /** + * @param $context ResourceLoaderContext + * @return bool + */ + public function isKnownEmpty( ResourceLoaderContext $context ) { + // Regardless of whether the files are specified, we always + // provide mw-wiki-logo styles. + return false; + } + + /** + * @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 ) { + $logo = $this->getConfig()->get( 'Logo' ); + $logoHD = $this->getConfig()->get( 'LogoHD' ); + return md5( parent::getModifiedHash( $context ) . $logo . json_encode( $logoHD ) ); + } +} diff --git a/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php new file mode 100644 index 00000000..5c917091 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderSpecialCharacterDataModule.php @@ -0,0 +1,107 @@ +<?php +/** + * Resource loader module for populating special characters data for some + * editing extensions to use. + * + * 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 + */ + +/** + * Resource loader module for populating special characters data for some + * editing extensions to use. + */ +class ResourceLoaderSpecialCharacterDataModule extends ResourceLoaderModule { + private $path = "resources/src/mediawiki.language/specialcharacters.json"; + protected $targets = array( 'desktop', 'mobile' ); + + /** + * Get all the dynamic data. + * + * @return array + */ + protected function getData() { + return json_decode( file_get_contents( $this->path ) ); + } + + /** + * @param ResourceLoaderContext $context + * @return string JavaScript code + */ + public function getScript( ResourceLoaderContext $context ) { + return Xml::encodeJsCall( + 'mw.language.setSpecialCharacters', + array( + $this->getData() + ), + ResourceLoader::inDebugMode() + ); + } + + /** + * @param ResourceLoaderContext $context + * @return int UNIX timestamp + */ + public function getModifiedTime( ResourceLoaderContext $context ) { + return static::safeFilemtime( $this->path ); + } + + /** + * @param ResourceLoaderContext $context + * @return string Hash + */ + public function getModifiedHash( ResourceLoaderContext $context ) { + return md5( serialize( $this->getData() ) ); + } + + /** + * @return array + */ + public function getDependencies() { + return array( 'mediawiki.language' ); + } + + /** + * @return array + */ + public function getMessages() { + return array( + 'special-characters-group-latin', + 'special-characters-group-latinextended', + 'special-characters-group-ipa', + 'special-characters-group-symbols', + 'special-characters-group-greek', + 'special-characters-group-cyrillic', + 'special-characters-group-arabic', + 'special-characters-group-arabicextended', + 'special-characters-group-persian', + 'special-characters-group-hebrew', + 'special-characters-group-bangla', + 'special-characters-group-tamil', + 'special-characters-group-telugu', + 'special-characters-group-sinhala', + 'special-characters-group-devanagari', + 'special-characters-group-gujarati', + 'special-characters-group-thai', + 'special-characters-group-lao', + 'special-characters-group-khmer', + 'special-characters-title-endash', + 'special-characters-title-emdash', + 'special-characters-title-minus' + ); + } +} diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index 78fe8e01..b2fbae9c 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -82,6 +82,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgServerName' => $conf->get( 'ServerName' ), 'wgUserLanguage' => $context->getLanguage(), 'wgContentLanguage' => $wgContLang->getCode(), + 'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ), 'wgVersion' => $conf->get( 'Version' ), 'wgEnableAPI' => $conf->get( 'EnableAPI' ), 'wgEnableWriteAPI' => $conf->get( 'EnableWriteAPI' ), @@ -90,11 +91,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgNamespaceIds' => $namespaceIds, 'wgContentNamespaces' => MWNamespace::getContentNamespaces(), 'wgSiteName' => $conf->get( 'Sitename' ), - 'wgFileExtensions' => array_values( array_unique( $conf->get( 'FileExtensions' ) ) ), 'wgDBname' => $conf->get( 'DBname' ), - // This sucks, it is only needed on Special:Upload, but I could - // not find a way to add vars only for a certain module - 'wgFileCanRotate' => SpecialUpload::rotationEnabled(), 'wgAvailableSkins' => Skin::getSkinNames(), 'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ), // MediaWiki sets cookies to have this prefix by default @@ -109,7 +106,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ), ); - wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) ); + Hooks::run( 'ResourceLoaderGetConfigVars', array( &$vars ) ); $this->configVars[$hash] = $vars; return $this->configVars[$hash]; @@ -150,7 +147,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } /** - * Optimize the dependency tree in $this->modules and return it. + * Optimize the dependency tree in $this->modules. * * The optimization basically works like this: * Given we have module A with the dependencies B and C @@ -158,11 +155,11 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { * Now we don't have to tell the client to explicitly fetch module * C as that's already included in module B. * - * This way we can reasonably reduce the amout of module registration + * This way we can reasonably reduce the amount of module registration * data send to the client. * * @param array &$registryData Modules keyed by name with properties: - * - string 'version' + * - number 'version' * - array 'dependencies' * - string|null 'group' * - string 'source' @@ -191,7 +188,6 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { * @return string JavaScript code for registering all modules with the client loader */ public function getModuleRegistrations( ResourceLoaderContext $context ) { - wfProfileIn( __METHOD__ ); $resourceLoader = $context->getResourceLoader(); $target = $context->getRequest()->getVal( 'target', 'desktop' ); @@ -214,12 +210,10 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { continue; } - // getModifiedTime() is supposed to return a UNIX timestamp, but it doesn't always - // seem to do that, and custom implementations might forget. Coerce it to TS_UNIX + // 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 ) ); - $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ) ); - - // FIXME: Convert to numbers, wfTimestamp always gives us stings, even for TS_UNIX $skipFunction = $module->getSkipFunction(); if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) { @@ -232,8 +226,14 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { ); } + $mtime = max( + $moduleMtime, + wfTimestamp( TS_UNIX, $this->getConfig()->get( 'CacheEpoch' ) ) + ); + $registryData[$name] = array( - 'version' => $mtime, + // Convert to numbers as wfTimestamp always returns a string, even for TS_UNIX + 'version' => (int) $mtime, 'dependencies' => $module->getDependencies(), 'group' => $module->getGroup(), 'source' => $module->getSource(), @@ -254,7 +254,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { if ( $data['loader'] !== false ) { $out .= ResourceLoader::makeCustomLoaderScript( $name, - wfTimestamp( TS_ISO_8601_BASIC, $data['version'] ), + $data['version'], $data['dependencies'], $data['group'], $data['source'], @@ -263,63 +263,21 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { continue; } - if ( - !count( $data['dependencies'] ) && - $data['group'] === null && - $data['source'] === 'local' && - $data['skip'] === null - ) { - // Modules with no dependencies, group, foreign source or skip function; - // call mw.loader.register(name, timestamp) - $registrations[] = array( $name, $data['version'] ); - } elseif ( - $data['group'] === null && - $data['source'] === 'local' && - $data['skip'] === null - ) { - // Modules with dependencies but no group, foreign source or skip function; - // call mw.loader.register(name, timestamp, dependencies) - $registrations[] = array( $name, $data['version'], $data['dependencies'] ); - } elseif ( - $data['source'] === 'local' && - $data['skip'] === null - ) { - // Modules with a group but no foreign source or skip function; - // call mw.loader.register(name, timestamp, dependencies, group) - $registrations[] = array( - $name, - $data['version'], - $data['dependencies'], - $data['group'] - ); - } elseif ( $data['skip'] === null ) { - // Modules with a foreign source but no skip function; - // call mw.loader.register(name, timestamp, dependencies, group, source) - $registrations[] = array( - $name, - $data['version'], - $data['dependencies'], - $data['group'], - $data['source'] - ); - } else { - // Modules with a skip function; - // call mw.loader.register(name, timestamp, dependencies, group, source, skip) - $registrations[] = array( - $name, - $data['version'], - $data['dependencies'], - $data['group'], - $data['source'], - $data['skip'] - ); - } + // Call mw.loader.register(name, timestamp, dependencies, group, source, skip) + $registrations[] = array( + $name, + $data['version'], + $data['dependencies'], + $data['group'], + // Swap default (local) for null + $data['source'] === 'local' ? null : $data['source'], + $data['skip'] + ); } // Register modules $out .= ResourceLoader::makeLoaderRegisterScript( $registrations ); - wfProfileOut( __METHOD__ ); return $out; } @@ -333,7 +291,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } /** - * Base modules required for the the base environment of ResourceLoader + * Base modules required for the base environment of ResourceLoader * * @return array */ @@ -355,7 +313,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // Get the latest version $loader = $context->getResourceLoader(); - $version = 0; + $version = 1; foreach ( $moduleNames as $moduleName ) { $version = max( $version, $loader->getModule( $moduleName )->getModifiedTime( $context ) @@ -390,18 +348,28 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $registrations = $this->getModuleRegistrations( $context ); // 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 " . - Xml::encodeJsCall( 'mw.Map', array( $this->getConfig()->get( 'LegacyJavaScriptGlobals' ) ) ) . "\n" . + $mwMapJsCall . "\n" . "\t$registrations\n" . - "\t" . Xml::encodeJsCall( 'mw.config.set', array( $configuration ) ) . + "\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; @@ -440,8 +408,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // 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 the loop over - // all modules below includes the the startup module (this module). + // 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 ) { diff --git a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php index 40274c63..472ceb26 100644 --- a/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php +++ b/includes/resourceloader/ResourceLoaderUserCSSPrefsModule.php @@ -42,8 +42,7 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { public function getModifiedTime( ResourceLoaderContext $context ) { $hash = $context->getHash(); if ( !isset( $this->modifiedTime[$hash] ) ) { - global $wgUser; - $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() ); + $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $context->getUserObj()->getTouched() ); } return $this->modifiedTime[$hash]; @@ -54,13 +53,11 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { * @return array */ public function getStyles( ResourceLoaderContext $context ) { - global $wgUser; - if ( !$this->getConfig()->get( 'AllowUserCssPrefs' ) ) { return array(); } - $options = $wgUser->getOptions(); + $options = $context->getUserObj()->getOptions(); // Build CSS rules $rules = array(); @@ -93,11 +90,4 @@ class ResourceLoaderUserCSSPrefsModule extends ResourceLoaderModule { public function getGroup() { return 'private'; } - - /** - * @return array - */ - public function getDependencies() { - return array( 'mediawiki.user' ); - } } diff --git a/includes/resourceloader/ResourceLoaderNoscriptModule.php b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php index 61927d77..5f4bc16b 100644 --- a/includes/resourceloader/ResourceLoaderNoscriptModule.php +++ b/includes/resourceloader/ResourceLoaderUserDefaultsModule.php @@ -1,6 +1,6 @@ <?php /** - * Resource loader for site customizations for users without JavaScript enabled. + * Resource loader module for default user preferences. * * 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 @@ -18,37 +18,45 @@ * http://www.gnu.org/copyleft/gpl.html * * @file - * @author Trevor Parscal - * @author Roan Kattouw + * @author Ori Livneh */ /** - * Module for site customizations + * Module for default user preferences. */ -class ResourceLoaderNoscriptModule extends ResourceLoaderWikiModule { +class ResourceLoaderUserDefaultsModule extends ResourceLoaderModule { - /* Protected Methods */ + /* Protected Members */ + + protected $targets = array( 'desktop', 'mobile' ); + + /* Methods */ /** - * Gets list of pages used by this module. Obviously, it makes absolutely no - * sense to include JavaScript files here... :D - * * @param ResourceLoaderContext $context - * - * @return array List of pages + * @return string Hash */ - protected function getPages( ResourceLoaderContext $context ) { - return array( 'MediaWiki:Noscript.css' => array( 'type' => 'style' ) ); + public function getModifiedHash( ResourceLoaderContext $context ) { + return md5( serialize( User::getDefaultOptions() ) ); } - /* Methods */ + /** + * @param ResourceLoaderContext $context + * @return int + */ + public function getModifiedTime( ResourceLoaderContext $context ) { + return $this->getHashMtime( $context ); + } /** - * Gets group name - * - * @return string Name of group + * @param ResourceLoaderContext $context + * @return string */ - public function getGroup() { - return 'noscript'; + public function getScript( ResourceLoaderContext $context ) { + return Xml::encodeJsCall( + 'mw.user.options.set', + array( User::getDefaultOptions() ), + ResourceLoader::inDebugMode() + ); } } diff --git a/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/includes/resourceloader/ResourceLoaderUserGroupsModule.php index 7cf19420..417cfced 100644 --- a/includes/resourceloader/ResourceLoaderUserGroupsModule.php +++ b/includes/resourceloader/ResourceLoaderUserGroupsModule.php @@ -25,39 +25,23 @@ */ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule { - /* Protected Members */ - protected $origin = self::ORIGIN_USER_SITEWIDE; protected $targets = array( 'desktop', 'mobile' ); - /* Protected Methods */ - /** * @param ResourceLoaderContext $context * @return array */ protected function getPages( ResourceLoaderContext $context ) { - global $wgUser; - - $userName = $context->getUser(); - if ( $userName === null ) { - return array(); - } - $useSiteJs = $this->getConfig()->get( 'UseSiteJs' ); $useSiteCss = $this->getConfig()->get( 'UseSiteCss' ); if ( !$useSiteJs && !$useSiteCss ) { return array(); } - // Use $wgUser is possible; allows to skip a lot of code - if ( is_object( $wgUser ) && $wgUser->getName() == $userName ) { - $user = $wgUser; - } else { - $user = User::newFromName( $userName ); - if ( !$user instanceof User ) { - return array(); - } + $user = $context->getUserObj(); + if ( !$user || $user->isAnon() ) { + return array(); } $pages = array(); @@ -75,9 +59,9 @@ class ResourceLoaderUserGroupsModule extends ResourceLoaderWikiModule { return $pages; } - /* Methods */ - /** + * Get group name + * * @return string */ public function getGroup() { diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php index 1b6d1de0..a0978445 100644 --- a/includes/resourceloader/ResourceLoaderUserModule.php +++ b/includes/resourceloader/ResourceLoaderUserModule.php @@ -27,47 +27,37 @@ */ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { - /* Protected Members */ - protected $origin = self::ORIGIN_USER_INDIVIDUAL; - /* Protected Methods */ - /** + * Get list of pages used by this module + * * @param ResourceLoaderContext $context - * @return array + * @return array List of pages */ protected function getPages( ResourceLoaderContext $context ) { - $username = $context->getUser(); - - if ( $username === null ) { - return array(); - } - $allowUserJs = $this->getConfig()->get( 'AllowUserJs' ); $allowUserCss = $this->getConfig()->get( 'AllowUserCss' ); - if ( !$allowUserJs && !$allowUserCss ) { return array(); } - // Get the normalized title of the user's user page - $userpageTitle = Title::makeTitleSafe( NS_USER, $username ); - - if ( !$userpageTitle instanceof Title ) { + $user = $context->getUserObj(); + if ( !$user || $user->isAnon() ) { return array(); } - $userpage = $userpageTitle->getPrefixedDBkey(); // Needed so $excludepages works + // Needed so $excludepages works + $userPage = $user->getUserPage()->getPrefixedDBkey(); $pages = array(); if ( $allowUserJs ) { - $pages["$userpage/common.js"] = array( 'type' => 'script' ); - $pages["$userpage/" . $context->getSkin() . '.js'] = array( 'type' => 'script' ); + $pages["$userPage/common.js"] = array( 'type' => 'script' ); + $pages["$userPage/" . $context->getSkin() . '.js'] = array( 'type' => 'script' ); } if ( $allowUserCss ) { - $pages["$userpage/common.css"] = array( 'type' => 'style' ); - $pages["$userpage/" . $context->getSkin() . '.css'] = array( 'type' => 'style' ); + $pages["$userPage/common.css"] = array( 'type' => 'style' ); + $pages["$userPage/" . $context->getSkin() . '.css'] = array( 'type' => 'style' ); } // Hack for bug 26283: if we're on a preview page for a CSS/JS page, @@ -82,9 +72,9 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { return $pages; } - /* Methods */ - /** + * Get group name + * * @return string */ public function getGroup() { diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index bd97a8e5..84c1906d 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -38,14 +38,20 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { /* Methods */ /** + * @return array List of module names as strings + */ + public function getDependencies() { + return array( 'user.defaults' ); + } + + /** * @param ResourceLoaderContext $context - * @return array|int|mixed + * @return int */ public function getModifiedTime( ResourceLoaderContext $context ) { $hash = $context->getHash(); if ( !isset( $this->modifiedTime[$hash] ) ) { - global $wgUser; - $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $wgUser->getTouched() ); + $this->modifiedTime[$hash] = wfTimestamp( TS_UNIX, $context->getUserObj()->getTouched() ); } return $this->modifiedTime[$hash]; @@ -56,9 +62,8 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { * @return string */ public function getScript( ResourceLoaderContext $context ) { - global $wgUser; return Xml::encodeJsCall( 'mw.user.options.set', - array( $wgUser->getOptions() ), + array( $context->getUserObj()->getOptions( User::GETOPTIONS_EXCLUDE_DEFAULTS ) ), ResourceLoader::inDebugMode() ); } diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php index 668467ca..ccd1dfd0 100644 --- a/includes/resourceloader/ResourceLoaderUserTokensModule.php +++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php @@ -37,15 +37,16 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule { /** * Fetch the tokens for the current user. * + * @param ResourceLoaderContext $context * @return array List of tokens keyed by token type */ - protected function contextUserTokens() { - global $wgUser; + protected function contextUserTokens( ResourceLoaderContext $context ) { + $user = $context->getUserObj(); return array( - 'editToken' => $wgUser->getEditToken(), - 'patrolToken' => $wgUser->getEditToken( 'patrol' ), - 'watchToken' => $wgUser->getEditToken( 'watch' ), + 'editToken' => $user->getEditToken(), + 'patrolToken' => $user->getEditToken( 'patrol' ), + 'watchToken' => $user->getEditToken( 'watch' ), ); } @@ -55,7 +56,7 @@ class ResourceLoaderUserTokensModule extends ResourceLoaderModule { */ public function getScript( ResourceLoaderContext $context ) { return Xml::encodeJsCall( 'mw.user.tokens.set', - array( $this->contextUserTokens() ), + array( $this->contextUserTokens( $context ) ), ResourceLoader::inDebugMode() ); } diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index de61fc55..7b44cc67 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -29,17 +29,37 @@ * because of its dependence on the functionality of * Title::isCssJsSubpage. */ -abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { +class ResourceLoaderWikiModule extends ResourceLoaderModule { - /* Protected Members */ - - # Origin is user-supplied code + // Origin defaults to users with sitewide authority protected $origin = self::ORIGIN_USER_SITEWIDE; // In-object cache for title info protected $titleInfo = array(); - /* Abstract Protected Methods */ + // List of page names that contain CSS + protected $styles = array(); + + // List of page names that contain JavaScript + protected $scripts = array(); + + // Group of module + protected $group; + + /** + * @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 ( isset( $options['scripts'] ) ) { + $this->scripts = $options['scripts']; + } + if ( isset( $options['group'] ) ) { + $this->group = $options['group']; + } + } /** * Subclasses should return an associative array of resources in the module. @@ -57,9 +77,34 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { * @param ResourceLoaderContext $context * @return array */ - abstract protected function getPages( ResourceLoaderContext $context ); + protected function getPages( ResourceLoaderContext $context ) { + $config = $this->getConfig(); + $pages = array(); - /* Protected Methods */ + // Filter out pages from origins not allowed by the current wiki configuration. + if ( $config->get( 'UseSiteJs' ) ) { + foreach ( $this->scripts as $script ) { + $pages[$script] = array( 'type' => 'script' ); + } + } + + if ( $config->get( 'UseSiteCss' ) ) { + foreach ( $this->styles as $style ) { + $pages[$style] = array( 'type' => 'style' ); + } + } + + return $pages; + } + + /** + * Get group name + * + * @return string + */ + public function getGroup() { + return $this->group; + } /** * Get the Database object used in getTitleMTimes(). Defaults to the local slave DB @@ -70,7 +115,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { * 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. * - * @return DatabaseBase|null + * @return IDatabase|null */ protected function getDB() { return wfGetDB( DB_SLAVE ); @@ -81,9 +126,15 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { * @return null|string */ protected function getContent( $title ) { - if ( !$title->isCssJsSubpage() && !$title->isCssOrJsPage() ) { + $handler = ContentHandler::getForTitle( $title ); + if ( $handler->isSupportedFormat( CONTENT_FORMAT_CSS ) ) { + $format = CONTENT_FORMAT_CSS; + } elseif ( $handler->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) { + $format = CONTENT_FORMAT_JAVASCRIPT; + } else { return null; } + $revision = Revision::newFromTitle( $title, false, Revision::READ_NORMAL ); if ( !$revision ) { return null; @@ -96,18 +147,9 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { return null; } - if ( $content->isSupportedFormat( CONTENT_FORMAT_JAVASCRIPT ) ) { - return $content->serialize( CONTENT_FORMAT_JAVASCRIPT ); - } elseif ( $content->isSupportedFormat( CONTENT_FORMAT_CSS ) ) { - return $content->serialize( CONTENT_FORMAT_CSS ); - } else { - wfDebugLog( 'resourceloader', __METHOD__ . ": bad content model {$content->getModel()} for JS/CSS page!" ); - return null; - } + return $content->serialize( $format ); } - /* Methods */ - /** * @param ResourceLoaderContext $context * @return string @@ -165,13 +207,13 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { /** * @param ResourceLoaderContext $context - * @return int|mixed + * @return int */ public function getModifiedTime( ResourceLoaderContext $context ) { - $modifiedTime = 1; // wfTimestamp() interprets 0 as "now" + $modifiedTime = 1; $titleInfo = $this->getTitleInfo( $context ); if ( count( $titleInfo ) ) { - $mtimes = array_map( function( $value ) { + $mtimes = array_map( function ( $value ) { return $value['timestamp']; }, $titleInfo ); $modifiedTime = max( $modifiedTime, max( $mtimes ) ); @@ -227,8 +269,8 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { * 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, with value is an array with 'length' and 'timestamp' - * keys, where the timestamp is a unix one + * @return array Keyed by page dbkey. Value is an array with 'length' and 'timestamp' + * keys, where the timestamp is a UNIX timestamp */ protected function getTitleInfo( ResourceLoaderContext $context ) { $dbr = $this->getDB(); |