* https://www.mediawiki.org/wiki/ResourceLoader
*/
class ResourceLoader implements LoggerAwareInterface {
- /** @var int */
- const CACHE_VERSION = 8;
+ /** @var Config $config */
+ protected $config;
+ /** @var MessageBlobStore */
+ protected $blobStore;
- /** @var bool */
- protected static $debugMode = null;
+ /** @var LoggerInterface */
+ private $logger;
- /**
- * Module name/ResourceLoaderModule object pairs
- * @var array
- */
+ /** @var ResourceLoaderModule[] Map of (module name => ResourceLoaderModule) */
protected $modules = [];
-
- /**
- * Associative array mapping module name to info associative array
- * @var array
- */
+ /** @var array[] Map of (module name => associative info array) */
protected $moduleInfos = [];
-
- /** @var Config $config */
- protected $config;
-
/**
* Associative array mapping framework ids to a list of names of test suite modules
* like [ 'qunit' => [ 'mediawiki.tests.qunit.suites', 'ext.foo.tests', ... ], ... ]
* @var array
*/
protected $testModuleNames = [];
+ /** @var string[] List of module names that contain QUnit test suites */
+ protected $testSuiteModuleNames = [];
- /**
- * E.g. [ 'source-id' => 'http://.../load.php' ]
- * @var array
- */
+ /** @var array Map of (source => path); E.g. [ 'source-id' => 'http://.../load.php' ] */
protected $sources = [];
-
- /**
- * Errors accumulated during current respond() call.
- * @var array
- */
+ /** @var array Errors accumulated during current respond() call */
protected $errors = [];
-
- /**
- * List of extra HTTP response headers provided by loaded modules.
- *
- * Populated by makeModuleResponse().
- *
- * @var array
- */
+ /** @var string[] Extra HTTP response headers from modules loaded in makeModuleResponse() */
protected $extraHeaders = [];
- /**
- * @var MessageBlobStore
- */
- protected $blobStore;
+ /** @var bool */
+ protected static $debugMode = null;
- /**
- * @var LoggerInterface
- */
- private $logger;
+ /** @var int */
+ const CACHE_VERSION = 8;
- /** @var string JavaScript / CSS pragma to disable minification. **/
+ /** @var string JavaScript / CSS pragma to disable minification. * */
const FILTER_NOMIN = '/*@nomin*/';
/**
/**
* 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
- * @param array|null $info Module info array. For backwards compatibility with 1.17alpha,
- * this may also be a ResourceLoaderModule object. Optional when using
- * multiple-registration calling style.
+ * @param string|array[] $name Module name as a string or, array of module info arrays
+ * keyed by name.
+ * @param array|null $info Module info array. When using the first parameter to register
+ * multiple modules at once, this parameter is optional.
* @throws MWException If a duplicate module registration is attempted
* @throws MWException If a module name contains illegal characters (pipes or commas)
- * @throws MWException If something other than a ResourceLoaderModule is being registered
+ * @throws InvalidArgumentException If the module info is not an array
*/
public function register( $name, $info = null ) {
$moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
);
}
- // Check $name for validity
+ // Check validity
if ( !self::isValidModuleName( $name ) ) {
throw new MWException( "ResourceLoader module name '$name' is invalid, "
. "see ResourceLoader::isValidModuleName()" );
}
-
- // Attach module
- if ( $info instanceof ResourceLoaderModule ) {
- $this->moduleInfos[$name] = [ 'object' => $info ];
- $info->setName( $name );
- $this->modules[$name] = $info;
- } elseif ( is_array( $info ) ) {
- // New calling convention
- $this->moduleInfos[$name] = $info;
- } else {
- throw new MWException(
- 'ResourceLoader module info type error for module \'' . $name .
- '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')'
+ if ( !is_array( $info ) ) {
+ throw new InvalidArgumentException(
+ 'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
);
}
- // Last-minute changes
+ // Attach module
+ $this->moduleInfos[$name] = $info;
+ // Last-minute changes
// Apply custom skin-defined styles to existing modules.
if ( $this->isFileModule( $name ) ) {
foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
/**
* @internal For use by ServiceWiring only
+ * @codeCoverageIgnore
*/
public function registerTestModules() {
global $IP;
. 'Edit your <code>LocalSettings.php</code> to enable it.' );
}
- $testModules = [
- 'qunit' => [],
- ];
+ // This has a 'qunit' key for compat with the below hook.
+ $testModulesMeta = [ 'qunit' => [] ];
// Get test suites from extensions
// Avoid PHP 7.1 warning from passing $this by reference
$rl = $this;
- Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$rl ] );
+ Hooks::run( 'ResourceLoaderTestModules', [ &$testModulesMeta, &$rl ] );
$extRegistry = ExtensionRegistry::getInstance();
// In case of conflict, the deprecated hook has precedence.
- $testModules['qunit'] += $extRegistry->getAttribute( 'QUnitTestModules' );
+ $testModules = $testModulesMeta['qunit'] + $extRegistry->getAttribute( 'QUnitTestModules' );
- // Add the QUnit testrunner as implicit dependency to extension test suites.
- foreach ( $testModules['qunit'] as &$module ) {
- // Shuck any single-module dependency as an array
+ $testSuiteModuleNames = [];
+ foreach ( $testModules as $name => &$module ) {
+ // Turn any single-module dependency into an array
if ( isset( $module['dependencies'] ) && is_string( $module['dependencies'] ) ) {
$module['dependencies'] = [ $module['dependencies'] ];
}
+ // Ensure the testrunner loads before any test suites
$module['dependencies'][] = 'test.mediawiki.qunit.testrunner';
- }
- // Get core test suites
- $testModules['qunit'] =
- ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit'];
+ // Keep track of the test suites to load on SpecialJavaScriptTest
+ $testSuiteModuleNames[] = $name;
+ }
- foreach ( $testModules as $id => $names ) {
- // Register test modules
- $this->register( $testModules[$id] );
+ // Core test suites (their names have further precedence).
+ $testModules = ( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules;
+ $testSuiteModuleNames[] = 'test.mediawiki.qunit.suites';
- // Keep track of their names so that they can be loaded together
- $this->testModuleNames[$id] = array_keys( $testModules[$id] );
- }
+ $this->register( $testModules );
+ $this->testSuiteModuleNames = $testSuiteModuleNames;
}
/**
}
/**
- * Get a list of test module names for one (or all) frameworks.
- *
- * If the given framework id is unknkown, or if the in-object variable is not an array,
- * then it will return an empty array.
+ * Get a list of module names with QUnit test suites.
*
- * @param string $framework Get only the test module names for one
- * particular framework (optional)
+ * @internal For use by SpecialJavaScriptTest only
* @return array
+ * @codeCoverageIgnore
*/
- public function getTestModuleNames( $framework = 'all' ) {
- /** @todo api siteinfo prop testmodulenames modulenames */
- if ( $framework == 'all' ) {
- return $this->testModuleNames;
- } elseif ( isset( $this->testModuleNames[$framework] )
- && is_array( $this->testModuleNames[$framework] )
- ) {
- return $this->testModuleNames[$framework];
- } else {
- return [];
- }
+ public function getTestSuiteModuleNames() {
+ return $this->testSuiteModuleNames;
}
/**
// No such module
return null;
}
- // Construct the requested object
+ // Construct the requested module object
$info = $this->moduleInfos[$name];
- /** @var ResourceLoaderModule $object */
- if ( isset( $info['object'] ) ) {
- // Object given in info array
- $object = $info['object'];
- } elseif ( isset( $info['factory'] ) ) {
+ if ( isset( $info['factory'] ) ) {
+ /** @var ResourceLoaderModule $object */
$object = call_user_func( $info['factory'], $info );
- $object->setConfig( $this->getConfig() );
- $object->setLogger( $this->logger );
} else {
$class = $info['class'] ?? ResourceLoaderFileModule::class;
/** @var ResourceLoaderModule $object */
$object = new $class( $info );
- $object->setConfig( $this->getConfig() );
- $object->setLogger( $this->logger );
}
+ $object->setConfig( $this->getConfig() );
+ $object->setLogger( $this->logger );
$object->setName( $name );
$this->modules[$name] = $object;
}
return false;
}
$info = $this->moduleInfos[$name];
- if ( isset( $info['object'] ) ) {
- return false;
- }
return !isset( $info['factory'] ) && (
// The implied default for 'class' is ResourceLoaderFileModule
!isset( $info['class'] ) ||
if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
return; // output handled
}
+ } else {
+ $fileCache = null;
}
// Generate a response
}
}
- // Save response to file cache unless there are errors
- if ( isset( $fileCache ) && !$this->errors && $missing === [] ) {
- // Cache single modules and images...and other requests if there are enough hits
- if ( ResourceFileCache::useFileCache( $context ) ) {
- if ( $fileCache->isCacheWorthy() ) {
- $fileCache->saveText( $response );
- } else {
- $fileCache->incrMissesRecent( $context->getRequest() );
- }
+ // Consider saving the response to file cache (unless there are errors).
+ if ( $fileCache &&
+ !$this->errors &&
+ $missing === [] &&
+ ResourceFileCache::useFileCache( $context )
+ ) {
+ if ( $fileCache->isCacheWorthy() ) {
+ // There were enough hits, save the response to the cache
+ $fileCache->saveText( $response );
+ } else {
+ $fileCache->incrMissesRecent( $context->getRequest() );
}
}
* @return string JavaScript code
*/
public static function makeMessageSetScript( $messages ) {
- return Xml::encodeJsCall(
- 'mw.messages.set',
- [ (object)$messages ],
- self::inDebugMode()
- );
+ return 'mw.messages.set('
+ . self::encodeJsonForScript( (object)$messages )
+ . ');';
}
/**
if ( !is_array( $states ) ) {
$states = [ $states => $state ];
}
- return Xml::encodeJsCall(
- 'mw.loader.state',
- [ $states ],
- self::inDebugMode()
- );
+ return 'mw.loader.state('
+ . self::encodeJsonForScript( $states )
+ . ');';
}
private static function isEmptyObject( stdClass $obj ) {
array_walk( $modules, [ self::class, 'trimArray' ] );
- return Xml::encodeJsCall(
- 'mw.loader.register',
- [ $modules ],
- self::inDebugMode()
- );
+ return 'mw.loader.register('
+ . self::encodeJsonForScript( $modules )
+ . ');';
}
/**
if ( !is_array( $sources ) ) {
$sources = [ $sources => $loadUrl ];
}
- return Xml::encodeJsCall(
- 'mw.loader.addSource',
- [ $sources ],
- self::inDebugMode()
- );
+ return 'mw.loader.addSource('
+ . self::encodeJsonForScript( $sources )
+ . ');';
}
/**