91 const FILTER_NOMIN =
'/*@nomin*/';
108 if ( !$moduleNames ) {
117 $vary =
"$skin|$lang";
118 $res =
$dbr->select(
'module_deps', [
'md_module',
'md_deps' ], [
119 'md_module' => $moduleNames,
125 $modulesWithDeps = [];
126 foreach (
$res as $row ) {
127 $module = $this->
getModule( $row->md_module );
132 $modulesWithDeps[] = $row->md_module;
136 foreach ( array_diff( $moduleNames, $modulesWithDeps )
as $name ) {
139 $this->
getModule( $name )->setFileDependencies( $context, [] );
148 foreach ( $moduleNames
as $name ) {
150 if ( $module && $module->getMessages() ) {
156 foreach ( $blobs
as $name =>
$blob ) {
179 if ( strpos( $data, ResourceLoader::FILTER_NOMIN ) !==
false ) {
184 return self::applyFilter( $filter, $data );
190 $key =
$cache->makeGlobalKey(
194 self::$filterCacheVersion, md5( $data )
199 $stats->increment(
"resourceloader_cache.$filter.miss" );
200 $result = self::applyFilter( $filter, $data );
203 $stats->increment(
"resourceloader_cache.$filter.hit" );
214 $data = trim( $data );
217 $data = ( $filter ===
'minify-css' )
238 $this->logger =
$logger ?:
new NullLogger();
241 $this->logger->debug( __METHOD__ .
' was called without providing a Config instance' );
253 $this->
register( include
"$IP/resources/Resources.php" );
254 $this->
register( include
"$IP/resources/ResourcesOOUI.php" );
256 $this->
register(
$config->get(
'ResourceModules' ) );
260 Hooks::run(
'ResourceLoaderRegisterModules', [ &$rl ] );
262 if (
$config->get(
'EnableJavaScriptTest' ) ===
true ) {
321 public function register(
$name, $info = null ) {
322 $moduleSkinStyles = $this->config->
get(
'ResourceModuleSkinStyles' );
326 foreach ( $registrations
as $name => $info ) {
328 if ( isset( $this->moduleInfos[
$name] ) ) {
330 $this->logger->warning(
331 'ResourceLoader duplicate registration warning. ' .
332 'Another module has already been registered as ' . $name
337 if ( !self::isValidModuleName( $name ) ) {
338 throw new MWException(
"ResourceLoader module name '$name' is invalid, "
339 .
"see ResourceLoader::isValidModuleName()" );
344 $this->moduleInfos[
$name] = [
'object' => $info ];
345 $info->setName( $name );
346 $this->modules[
$name] = $info;
347 } elseif ( is_array( $info ) ) {
349 $this->moduleInfos[
$name] = $info;
352 'ResourceLoader module info type error for module \'' . $name .
353 '\': expected ResourceLoaderModule
or array (got:
' . gettype( $info ) . ')
'
357 // Last-minute changes
359 // Apply custom skin-defined styles to existing modules.
360 if ( $this->isFileModule( $name ) ) {
361 foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
362 // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
363 if ( isset( $this->moduleInfos[$name]['skinStyles
'][$skinName] ) ) {
367 // If $name is preceded with a '+
', the defined style files will be added to 'default'
368 // skinStyles, otherwise 'default' will be ignored as it normally would be.
369 if ( isset( $skinStyles[$name] ) ) {
370 $paths = (array)$skinStyles[$name];
372 } elseif ( isset( $skinStyles['+
' . $name] ) ) {
373 $paths = (array)$skinStyles['+
' . $name];
374 $styleFiles = isset( $this->moduleInfos[$name]['skinStyles
']['default'] ) ?
375 (array)$this->moduleInfos[$name]['skinStyles
']['default'] :
381 // Add new file paths, remapping them to refer to our directories and not use settings
383 list( $localBasePath, $remoteBasePath ) =
390 $this->moduleInfos[
$name][
'skinStyles'][$skinName] = $styleFiles;
402 if ( $this->config->get(
'EnableJavaScriptTest' ) !==
true ) {
403 throw new MWException(
'Attempt to register JavaScript test modules '
404 .
'but <code>$wgEnableJavaScriptTest</code> is false. '
405 .
'Edit your <code>LocalSettings.php</code> to enable it.' );
410 $testModules[
'qunit'] = [];
414 Hooks::run(
'ResourceLoaderTestModules', [ &$testModules, &$rl ] );
418 foreach ( $testModules[
'qunit']
as &$module ) {
422 $module[
'position'] =
'top';
423 $module[
'dependencies'][] =
'test.mediawiki.qunit.testrunner';
426 $testModules[
'qunit'] =
427 ( include
"$IP/tests/qunit/QUnitTestResources.php" ) + $testModules[
'qunit'];
429 foreach ( $testModules
as $id => $names ) {
431 $this->
register( $testModules[$id] );
434 $this->testModuleNames[$id] = array_keys( $testModules[$id] );
451 if ( is_array( $id ) ) {
453 $this->addSource( $key,
$value );
459 if ( isset( $this->sources[$id] ) ) {
461 'ResourceLoader duplicate source addition error. ' .
462 'Another source has already been registered as ' . $id
467 if ( is_array( $loadUrl ) ) {
468 if ( !isset( $loadUrl[
'loadScript'] ) ) {
470 __METHOD__ .
' was passed an array with no "loadScript" key.'
474 $loadUrl = $loadUrl[
'loadScript'];
477 $this->sources[$id] = $loadUrl;
486 return array_keys( $this->moduleInfos );
501 if ( $framework ==
'all' ) {
502 return $this->testModuleNames;
503 } elseif ( isset( $this->testModuleNames[$framework] )
504 && is_array( $this->testModuleNames[$framework] )
506 return $this->testModuleNames[$framework];
520 return isset( $this->moduleInfos[
$name] );
535 if ( !isset( $this->modules[
$name] ) ) {
536 if ( !isset( $this->moduleInfos[$name] ) ) {
541 $info = $this->moduleInfos[
$name];
543 if ( isset( $info[
'object'] ) ) {
545 $object = $info[
'object'];
547 if ( !isset( $info[
'class'] ) ) {
548 $class =
'ResourceLoaderFileModule';
550 $class = $info[
'class'];
553 $object =
new $class( $info );
554 $object->setConfig( $this->getConfig() );
555 $object->setLogger( $this->logger );
557 $object->setName( $name );
558 $this->modules[
$name] = $object;
561 return $this->modules[
$name];
571 if ( !isset( $this->moduleInfos[
$name] ) ) {
574 $info = $this->moduleInfos[
$name];
575 if ( isset( $info[
'object'] ) || isset( $info[
'class'] ) ) {
587 return $this->sources;
600 if ( !isset( $this->sources[
$source] ) ) {
601 throw new MWException(
"The $source source was never registered in ResourceLoader." );
603 return $this->sources[
$source];
612 $hash = hash(
'fnv132',
$value );
613 return Wikimedia\base_convert( $hash, 16, 36, 7 );
625 if ( !$moduleNames ) {
628 $hashes = array_map(
function ( $module )
use ( $context ) {
629 return $this->getModule( $module )->getVersionHash( $context );
631 return self::makeHash( implode(
'',
$hashes ) );
656 if ( !$this->getModule(
$name ) ) {
661 $moduleNames[] =
$name;
663 return $this->getCombinedVersion( $context, $moduleNames );
685 $module = $this->getModule(
$name );
689 if ( $module->getGroup() ===
'private' ) {
690 $this->logger->debug(
"Request for private module '$name' denied" );
691 $this->
errors[] =
"Cannot show private module \"$name\"";
702 $this->preloadModuleInfo( array_keys(
$modules ), $context );
705 $this->logger->warning(
'Preloading module info failed: {exception}', [
708 $this->
errors[] = self::formatExceptionNoComment( $e );
714 $versionHash = $this->getCombinedVersion( $context, array_keys(
$modules ) );
717 $this->logger->warning(
'Calculating version hash failed: {exception}', [
720 $this->
errors[] = self::formatExceptionNoComment( $e );
725 $etag =
'W/"' . $versionHash .
'"';
728 if ( $this->tryRespondNotModified( $context, $etag ) ) {
733 if ( $this->config->get(
'UseFileCache' ) ) {
735 if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
746 $warnings = ob_get_contents();
747 if ( strlen( $warnings ) ) {
748 $this->
errors[] = $warnings;
753 if ( isset( $fileCache ) && !$this->
errors && !count( $missing ) ) {
756 if ( $fileCache->isCacheWorthy() ) {
759 $fileCache->incrMissesRecent( $context->
getRequest() );
764 $this->sendResponseHeaders( $context, $etag, (
bool)$this->
errors );
771 $response = implode(
"\n\n", $this->errors );
772 } elseif ( $this->errors ) {
773 $errorText = implode(
"\n\n", $this->errors );
774 $errorResponse = self::makeComment( $errorText );
776 $errorResponse .=
'if (window.console && console.error) {'
801 $rlMaxage = $this->config->get(
'ResourceLoaderMaxage' );
808 || $context->
getVersion() !== $this->makeVersionQuery( $context )
810 $maxage = $rlMaxage[
'unversioned'][
'client'];
811 $smaxage = $rlMaxage[
'unversioned'][
'server'];
815 $maxage = $rlMaxage[
'versioned'][
'client'];
816 $smaxage = $rlMaxage[
'versioned'][
'server'];
821 header(
'Content-Type: text/plain; charset=utf-8' );
823 $context->
getImageObj()->sendResponseHeaders( $context );
825 } elseif ( $context->
getOnly() ===
'styles' ) {
826 header(
'Content-Type: text/css; charset=utf-8' );
827 header(
'Access-Control-Allow-Origin: *' );
829 header(
'Content-Type: text/javascript; charset=utf-8' );
833 header(
'ETag: ' . $etag );
836 header(
'Cache-Control: private, no-cache, must-revalidate' );
837 header(
'Pragma: no-cache' );
839 header(
"Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
840 $exp = min( $maxage, $smaxage );
860 if ( $clientKeys !==
false && !$context->
getDebug() && in_array( $etag, $clientKeys ) ) {
874 $this->sendResponseHeaders( $context, $etag,
false );
893 $rlMaxage = $this->config->get(
'ResourceLoaderMaxage' );
898 ? $rlMaxage[
'unversioned'][
'server']
899 : $rlMaxage[
'versioned'][
'server'];
912 $this->sendResponseHeaders( $context, $etag,
false );
917 $warnings = ob_get_contents();
918 if ( strlen( $warnings ) ) {
942 $encText = str_replace(
'*/',
'* /', $text );
943 return "/*\n$encText\n*/\n";
953 return self::makeComment( self::formatExceptionNoComment(
$e ) );
964 global $wgShowExceptionDetails;
966 if ( !$wgShowExceptionDetails ) {
987 if ( !count( $modules ) && !count( $missing ) ) {
997 $data =
$image->getImageData( $context );
998 if ( $data ===
false ) {
1000 $this->
errors[] =
'Image generation failed';
1005 foreach ( $missing
as $name ) {
1006 $states[
$name] =
'missing';
1012 $filter = $context->
getOnly() ===
'styles' ?
'minify-css' :
'minify-js';
1014 foreach ( $modules
as $name => $module ) {
1016 $content = $module->getModuleContent( $context );
1017 $implementKey = $name .
'@' . $module->getVersionHash( $context );
1021 switch ( $context->
getOnly() ) {
1024 if ( is_string( $scripts ) ) {
1026 $strContent = $scripts;
1027 } elseif ( is_array( $scripts ) ) {
1029 $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
1037 $strContent = isset( $styles[
'css'] ) ? implode(
'', $styles[
'css'] ) :
'';
1041 if ( is_string( $scripts ) ) {
1042 if ( $name ===
'site' || $name ===
'user' ) {
1048 $scripts = self::filter(
'minify-js', $scripts );
1051 $scripts =
new XmlJsCode( $scripts );
1054 $strContent = self::makeLoaderImplementScript(
1058 isset( $content[
'messagesBlob'] ) ?
new XmlJsCode( $content[
'messagesBlob'] ) : [],
1059 isset( $content[
'templates'] ) ? $content[
'templates'] : []
1065 $strContent = self::filter( $filter, $strContent );
1068 $out .= $strContent;
1072 $this->logger->warning(
'Generating module package failed: {exception}', [
1075 $this->
errors[] = self::formatExceptionNoComment( $e );
1078 $states[
$name] =
'error';
1079 unset( $modules[$name] );
1081 $isRaw |= $module->isRaw();
1086 if ( count( $modules ) && $context->
getOnly() ===
'scripts' ) {
1089 foreach ( $modules
as $name => $module ) {
1090 $states[
$name] =
'ready';
1095 if ( count( $states ) ) {
1096 $stateScript = self::makeLoaderStateScript( $states );
1098 $stateScript = self::filter(
'minify-js', $stateScript );
1100 $out .= $stateScript;
1103 if ( count( $states ) ) {
1104 $this->
errors[] =
'Problematic modules: ' .
1118 public function getModulesByMessage( $messageKey ) {
1120 foreach ( $this->getModuleNames()
as $moduleName ) {
1121 $module = $this->getModule( $moduleName );
1122 if ( in_array( $messageKey, $module->getMessages() ) ) {
1123 $moduleNames[] = $moduleName;
1126 return $moduleNames;
1147 protected static function makeLoaderImplementScript(
1148 $name, $scripts, $styles, $messages, $templates
1151 if ( $scripts instanceof XmlJsCode ) {
1152 $scripts =
new XmlJsCode(
"function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1153 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1154 throw new MWException(
'Invalid scripts error. Array of URLs or string of code expected.' );
1166 self::trimArray( $module );
1178 public static function makeMessageSetScript( $messages ) {
1181 [ (
object)$messages ],
1193 public static function makeCombinedStyles(
array $stylePairs ) {
1195 foreach ( $stylePairs
as $media => $styles ) {
1199 $styles = (
array)$styles;
1200 foreach ( $styles
as $style ) {
1201 $style = trim( $style );
1203 if ( $style !==
'' ) {
1208 if ( $media ===
'' || $media ==
'all' ) {
1210 } elseif ( is_string( $media ) ) {
1211 $out[] =
"@media $media {\n" . str_replace(
"\n",
"\n\t",
"\t" . $style ) .
"}";
1234 public static function makeLoaderStateScript(
$name, $state = null ) {
1235 if ( is_array(
$name ) ) {
1264 public static function makeCustomLoaderScript(
$name, $version, $dependencies,
1267 $script = str_replace(
"\n",
"\n\t", trim( $script ) );
1269 "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
1275 private static function isEmptyObject( stdClass $obj ) {
1276 foreach ( $obj
as $key =>
$value ) {
1294 private static function trimArray(
array &$array ) {
1295 $i = count( $array );
1297 if ( $array[$i] === null
1298 || $array[$i] === []
1299 || ( $array[$i] instanceof XmlJsCode && $array[$i]->value ===
'{}' )
1300 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1302 unset( $array[$i] );
1336 public static function makeLoaderRegisterScript(
$name, $version = null,
1337 $dependencies = null, $group = null,
$source = null, $skip = null
1339 if ( is_array(
$name ) ) {
1342 foreach (
$name as $i => &$module ) {
1343 $index[$module[0]] = $i;
1348 foreach (
$name as &$module ) {
1349 if ( isset( $module[2] ) ) {
1350 foreach ( $module[2]
as &$dependency ) {
1351 if ( isset( $index[$dependency] ) ) {
1352 $dependency = $index[$dependency];
1358 array_walk(
$name, [
'self',
'trimArray' ] );
1361 'mw.loader.register',
1366 $registration = [
$name, $version, $dependencies, $group,
$source, $skip ];
1367 self::trimArray( $registration );
1369 'mw.loader.register',
1390 public static function makeLoaderSourcesScript( $id, $loadUrl = null ) {
1391 if ( is_array( $id ) ) {
1393 'mw.loader.addSource',
1399 'mw.loader.addSource',
1414 public static function makeLoaderConditionalScript( $script ) {
1415 return '(window.RLQ=window.RLQ||[]).push(function(){' .
1416 trim( $script ) .
'});';
1428 public static function makeInlineScript( $script ) {
1429 $js = self::makeLoaderConditionalScript( $script );
1430 return new WrappedString(
1432 '<script>(window.RLQ=window.RLQ||[]).push(function(){',
1444 public static function makeConfigSetScript(
array $configuration ) {
1460 public static function makePackedModulesString(
$modules ) {
1463 $pos = strrpos( $module,
'.' );
1464 $prefix = $pos ===
false ?
'' : substr( $module, 0, $pos );
1465 $suffix = $pos ===
false ? $module : substr( $module, $pos + 1 );
1466 $groups[$prefix][] = $suffix;
1470 foreach ( $groups
as $prefix => $suffixes ) {
1471 $p = $prefix ===
'' ?
'' : $prefix .
'.';
1472 $arr[] = $p . implode(
',', $suffixes );
1474 $str = implode(
'|', $arr );
1483 public static function inDebugMode() {
1484 if ( self::$debugMode === null ) {
1486 self::$debugMode = $wgRequest->getFuzzyBool(
'debug',
1487 $wgRequest->getCookie(
'resourceLoaderDebug',
'', $wgResourceLoaderDebug )
1490 return self::$debugMode;
1500 public static function clearCache() {
1501 self::$debugMode = null;
1516 $query = self::createLoaderQuery( $context, $extraQuery );
1517 $script = $this->getLoadScript(
$source );
1532 return self::makeLoaderQuery(
1540 $context->
getRequest()->getBool(
'printable' ),
1541 $context->
getRequest()->getBool(
'handheld' ),
1564 $version = null,
$debug =
false, $only = null, $printable =
false,
1565 $handheld =
false, $extraQuery = []
1568 'modules' => self::makePackedModulesString(
$modules ),
1571 'debug' =>
$debug ?
'true' :
'false',
1573 if (
$user !== null ) {
1576 if ( $version !== null ) {
1577 $query[
'version'] = $version;
1579 if ( $only !== null ) {
1604 public static function isValidModuleName( $moduleName ) {
1605 return strcspn( $moduleName,
'!,|', 0, 255 ) === strlen( $moduleName );
1617 public function getLessCompiler( $extraVars = [] ) {
1621 if ( !class_exists(
'Less_Parser' ) ) {
1622 throw new MWException(
'MediaWiki requires the less.php parser' );
1626 $parser->ModifyVars( array_merge( $this->getLessVars(), $extraVars ) );
1628 array_fill_keys( $this->config->get(
'ResourceLoaderLESSImportPaths' ),
'' )
1630 $parser->SetOption(
'relativeUrls',
false );
1641 public function getLessVars() {
1642 if ( !$this->lessVars ) {
1643 $lessVars = $this->config->get(
'ResourceLoaderLESSVars' );
1644 Hooks::run(
'ResourceLoaderGetLessVars', [ &$lessVars ] );
1645 $this->lessVars = $lessVars;
1647 return $this->lessVars;
#define the
table suitable for use with IDatabase::select()
This class generates message blobs for use by ResourceLoader modules.
getModuleNames()
Get a list of module names.
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
array $modules
Module name/ResourceLoaderModule object pairs.
wfGetDB($db, $groups=[], $wiki=false)
Get a Database object.
static inlineScript($contents)
Output a "