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' ) );
257 Hooks::run(
'ResourceLoaderRegisterModules', [ &$this ] );
259 if (
$config->get(
'EnableJavaScriptTest' ) ===
true ) {
318 public function register(
$name, $info = null ) {
319 $moduleSkinStyles = $this->config->
get(
'ResourceModuleSkinStyles' );
323 foreach ( $registrations
as $name => $info ) {
325 if ( isset( $this->moduleInfos[
$name] ) ) {
327 $this->logger->warning(
328 'ResourceLoader duplicate registration warning. ' .
329 'Another module has already been registered as ' . $name
334 if ( !self::isValidModuleName( $name ) ) {
335 throw new MWException(
"ResourceLoader module name '$name' is invalid, "
336 .
"see ResourceLoader::isValidModuleName()" );
341 $this->moduleInfos[
$name] = [
'object' => $info ];
342 $info->setName( $name );
343 $this->modules[
$name] = $info;
344 } elseif ( is_array( $info ) ) {
346 $this->moduleInfos[
$name] = $info;
349 'ResourceLoader module info type error for module \'' . $name .
350 '\': expected ResourceLoaderModule
or array (got:
' . gettype( $info ) . ')
'
354 // Last-minute changes
356 // Apply custom skin-defined styles to existing modules.
357 if ( $this->isFileModule( $name ) ) {
358 foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
359 // If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles.
360 if ( isset( $this->moduleInfos[$name]['skinStyles
'][$skinName] ) ) {
364 // If $name is preceded with a '+
', the defined style files will be added to 'default'
365 // skinStyles, otherwise 'default' will be ignored as it normally would be.
366 if ( isset( $skinStyles[$name] ) ) {
367 $paths = (array)$skinStyles[$name];
369 } elseif ( isset( $skinStyles['+
' . $name] ) ) {
370 $paths = (array)$skinStyles['+
' . $name];
371 $styleFiles = isset( $this->moduleInfos[$name]['skinStyles
']['default'] ) ?
372 (array)$this->moduleInfos[$name]['skinStyles
']['default'] :
378 // Add new file paths, remapping them to refer to our directories and not use settings
380 list( $localBasePath, $remoteBasePath ) =
387 $this->moduleInfos[
$name][
'skinStyles'][$skinName] = $styleFiles;
399 if ( $this->config->get(
'EnableJavaScriptTest' ) !==
true ) {
400 throw new MWException(
'Attempt to register JavaScript test modules '
401 .
'but <code>$wgEnableJavaScriptTest</code> is false. '
402 .
'Edit your <code>LocalSettings.php</code> to enable it.' );
407 $testModules[
'qunit'] = [];
409 Hooks::run(
'ResourceLoaderTestModules', [ &$testModules, &$this ] );
413 foreach ( $testModules[
'qunit']
as &$module ) {
417 $module[
'position'] =
'top';
418 $module[
'dependencies'][] =
'test.mediawiki.qunit.testrunner';
421 $testModules[
'qunit'] =
422 ( include
"$IP/tests/qunit/QUnitTestResources.php" ) + $testModules[
'qunit'];
424 foreach ( $testModules
as $id => $names ) {
426 $this->
register( $testModules[$id] );
429 $this->testModuleNames[$id] = array_keys( $testModules[$id] );
446 if ( is_array( $id ) ) {
448 $this->addSource( $key,
$value );
454 if ( isset( $this->sources[$id] ) ) {
456 'ResourceLoader duplicate source addition error. ' .
457 'Another source has already been registered as ' . $id
462 if ( is_array( $loadUrl ) ) {
463 if ( !isset( $loadUrl[
'loadScript'] ) ) {
465 __METHOD__ .
' was passed an array with no "loadScript" key.'
469 $loadUrl = $loadUrl[
'loadScript'];
472 $this->sources[$id] = $loadUrl;
481 return array_keys( $this->moduleInfos );
496 if ( $framework ==
'all' ) {
497 return $this->testModuleNames;
498 } elseif ( isset( $this->testModuleNames[$framework] )
499 && is_array( $this->testModuleNames[$framework] )
501 return $this->testModuleNames[$framework];
515 return isset( $this->moduleInfos[
$name] );
530 if ( !isset( $this->modules[
$name] ) ) {
531 if ( !isset( $this->moduleInfos[$name] ) ) {
536 $info = $this->moduleInfos[
$name];
538 if ( isset( $info[
'object'] ) ) {
540 $object = $info[
'object'];
542 if ( !isset( $info[
'class'] ) ) {
543 $class =
'ResourceLoaderFileModule';
545 $class = $info[
'class'];
548 $object =
new $class( $info );
549 $object->setConfig( $this->getConfig() );
550 $object->setLogger( $this->logger );
552 $object->setName( $name );
553 $this->modules[
$name] = $object;
556 return $this->modules[
$name];
566 if ( !isset( $this->moduleInfos[
$name] ) ) {
569 $info = $this->moduleInfos[
$name];
570 if ( isset( $info[
'object'] ) || isset( $info[
'class'] ) ) {
582 return $this->sources;
595 if ( !isset( $this->sources[
$source] ) ) {
596 throw new MWException(
"The $source source was never registered in ResourceLoader." );
598 return $this->sources[
$source];
607 $hash = hash(
'fnv132',
$value );
608 return Wikimedia\base_convert( $hash, 16, 36, 7 );
620 if ( !$moduleNames ) {
623 $hashes = array_map(
function ( $module )
use ( $context ) {
624 return $this->getModule( $module )->getVersionHash( $context );
626 return self::makeHash( implode(
'',
$hashes ) );
651 if ( !$this->getModule(
$name ) ) {
656 $moduleNames[] =
$name;
658 return $this->getCombinedVersion( $context, $moduleNames );
680 $module = $this->getModule(
$name );
684 if ( $module->getGroup() ===
'private' ) {
685 $this->logger->debug(
"Request for private module '$name' denied" );
686 $this->
errors[] =
"Cannot show private module \"$name\"";
697 $this->preloadModuleInfo( array_keys(
$modules ), $context );
700 $this->logger->warning(
'Preloading module info failed: {exception}', [
703 $this->
errors[] = self::formatExceptionNoComment( $e );
709 $versionHash = $this->getCombinedVersion( $context, array_keys(
$modules ) );
712 $this->logger->warning(
'Calculating version hash failed: {exception}', [
715 $this->
errors[] = self::formatExceptionNoComment( $e );
720 $etag =
'W/"' . $versionHash .
'"';
723 if ( $this->tryRespondNotModified( $context, $etag ) ) {
728 if ( $this->config->get(
'UseFileCache' ) ) {
730 if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
741 $warnings = ob_get_contents();
742 if ( strlen( $warnings ) ) {
743 $this->
errors[] = $warnings;
748 if ( isset( $fileCache ) && !$this->
errors && !count( $missing ) ) {
751 if ( $fileCache->isCacheWorthy() ) {
754 $fileCache->incrMissesRecent( $context->
getRequest() );
759 $this->sendResponseHeaders( $context, $etag, (
bool)$this->
errors );
766 $response = implode(
"\n\n", $this->errors );
767 } elseif ( $this->errors ) {
768 $errorText = implode(
"\n\n", $this->errors );
769 $errorResponse = self::makeComment( $errorText );
771 $errorResponse .=
'if (window.console && console.error) {'
796 $rlMaxage = $this->config->get(
'ResourceLoaderMaxage' );
803 || $context->
getVersion() !== $this->makeVersionQuery( $context )
805 $maxage = $rlMaxage[
'unversioned'][
'client'];
806 $smaxage = $rlMaxage[
'unversioned'][
'server'];
810 $maxage = $rlMaxage[
'versioned'][
'client'];
811 $smaxage = $rlMaxage[
'versioned'][
'server'];
816 header(
'Content-Type: text/plain; charset=utf-8' );
818 $context->
getImageObj()->sendResponseHeaders( $context );
820 } elseif ( $context->
getOnly() ===
'styles' ) {
821 header(
'Content-Type: text/css; charset=utf-8' );
822 header(
'Access-Control-Allow-Origin: *' );
824 header(
'Content-Type: text/javascript; charset=utf-8' );
828 header(
'ETag: ' . $etag );
831 header(
'Cache-Control: private, no-cache, must-revalidate' );
832 header(
'Pragma: no-cache' );
834 header(
"Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
835 $exp = min( $maxage, $smaxage );
855 if ( $clientKeys !==
false && !$context->
getDebug() && in_array( $etag, $clientKeys ) ) {
869 $this->sendResponseHeaders( $context, $etag,
false );
888 $rlMaxage = $this->config->get(
'ResourceLoaderMaxage' );
893 ? $rlMaxage[
'unversioned'][
'server']
894 : $rlMaxage[
'versioned'][
'server'];
907 $this->sendResponseHeaders( $context, $etag,
false );
912 $warnings = ob_get_contents();
913 if ( strlen( $warnings ) ) {
937 $encText = str_replace(
'*/',
'* /', $text );
938 return "/*\n$encText\n*/\n";
948 return self::makeComment( self::formatExceptionNoComment(
$e ) );
959 global $wgShowExceptionDetails;
961 if ( !$wgShowExceptionDetails ) {
982 if ( !count( $modules ) && !count( $missing ) ) {
992 $data =
$image->getImageData( $context );
993 if ( $data ===
false ) {
995 $this->
errors[] =
'Image generation failed';
1000 foreach ( $missing
as $name ) {
1001 $states[
$name] =
'missing';
1007 $filter = $context->
getOnly() ===
'styles' ?
'minify-css' :
'minify-js';
1009 foreach ( $modules
as $name => $module ) {
1011 $content = $module->getModuleContent( $context );
1012 $implementKey = $name .
'@' . $module->getVersionHash( $context );
1016 switch ( $context->
getOnly() ) {
1019 if ( is_string( $scripts ) ) {
1021 $strContent = $scripts;
1022 } elseif ( is_array( $scripts ) ) {
1024 $strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] );
1032 $strContent = isset( $styles[
'css'] ) ? implode(
'', $styles[
'css'] ) :
'';
1036 if ( is_string( $scripts ) ) {
1037 if ( $name ===
'site' || $name ===
'user' ) {
1043 $scripts = self::filter(
'minify-js', $scripts );
1046 $scripts =
new XmlJsCode( $scripts );
1049 $strContent = self::makeLoaderImplementScript(
1053 isset( $content[
'messagesBlob'] ) ?
new XmlJsCode( $content[
'messagesBlob'] ) : [],
1054 isset( $content[
'templates'] ) ? $content[
'templates'] : []
1060 $strContent = self::filter( $filter, $strContent );
1063 $out .= $strContent;
1067 $this->logger->warning(
'Generating module package failed: {exception}', [
1070 $this->
errors[] = self::formatExceptionNoComment( $e );
1073 $states[
$name] =
'error';
1074 unset( $modules[$name] );
1076 $isRaw |= $module->isRaw();
1081 if ( count( $modules ) && $context->
getOnly() ===
'scripts' ) {
1084 foreach ( $modules
as $name => $module ) {
1085 $states[
$name] =
'ready';
1090 if ( count( $states ) ) {
1091 $stateScript = self::makeLoaderStateScript( $states );
1093 $stateScript = self::filter(
'minify-js', $stateScript );
1095 $out .= $stateScript;
1098 if ( count( $states ) ) {
1099 $this->
errors[] =
'Problematic modules: ' .
1113 public function getModulesByMessage( $messageKey ) {
1115 foreach ( $this->getModuleNames()
as $moduleName ) {
1116 $module = $this->getModule( $moduleName );
1117 if ( in_array( $messageKey, $module->getMessages() ) ) {
1118 $moduleNames[] = $moduleName;
1121 return $moduleNames;
1142 protected static function makeLoaderImplementScript(
1143 $name, $scripts, $styles, $messages, $templates
1146 if ( $scripts instanceof XmlJsCode ) {
1147 $scripts =
new XmlJsCode(
"function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1148 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1149 throw new MWException(
'Invalid scripts error. Array of URLs or string of code expected.' );
1161 self::trimArray( $module );
1173 public static function makeMessageSetScript( $messages ) {
1176 [ (
object)$messages ],
1188 public static function makeCombinedStyles(
array $stylePairs ) {
1190 foreach ( $stylePairs
as $media => $styles ) {
1194 $styles = (
array)$styles;
1195 foreach ( $styles
as $style ) {
1196 $style = trim( $style );
1198 if ( $style !==
'' ) {
1203 if ( $media ===
'' || $media ==
'all' ) {
1205 } elseif ( is_string( $media ) ) {
1206 $out[] =
"@media $media {\n" . str_replace(
"\n",
"\n\t",
"\t" . $style ) .
"}";
1229 public static function makeLoaderStateScript(
$name, $state = null ) {
1230 if ( is_array(
$name ) ) {
1259 public static function makeCustomLoaderScript(
$name, $version, $dependencies,
1262 $script = str_replace(
"\n",
"\n\t", trim( $script ) );
1264 "( function ( name, version, dependencies, group, source ) {\n\t$script\n} )",
1270 private static function isEmptyObject( stdClass $obj ) {
1271 foreach ( $obj
as $key =>
$value ) {
1289 private static function trimArray(
array &$array ) {
1290 $i = count( $array );
1292 if ( $array[$i] === null
1293 || $array[$i] === []
1294 || ( $array[$i] instanceof XmlJsCode && $array[$i]->value ===
'{}' )
1295 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1297 unset( $array[$i] );
1331 public static function makeLoaderRegisterScript(
$name, $version = null,
1332 $dependencies = null, $group = null,
$source = null, $skip = null
1334 if ( is_array(
$name ) ) {
1337 foreach (
$name as $i => &$module ) {
1338 $index[$module[0]] = $i;
1343 foreach (
$name as &$module ) {
1344 if ( isset( $module[2] ) ) {
1345 foreach ( $module[2]
as &$dependency ) {
1346 if ( isset( $index[$dependency] ) ) {
1347 $dependency = $index[$dependency];
1353 array_walk(
$name, [
'self',
'trimArray' ] );
1356 'mw.loader.register',
1361 $registration = [
$name, $version, $dependencies, $group,
$source, $skip ];
1362 self::trimArray( $registration );
1364 'mw.loader.register',
1385 public static function makeLoaderSourcesScript( $id, $loadUrl = null ) {
1386 if ( is_array( $id ) ) {
1388 'mw.loader.addSource',
1394 'mw.loader.addSource',
1409 public static function makeLoaderConditionalScript( $script ) {
1410 return '(window.RLQ=window.RLQ||[]).push(function(){' .
1411 trim( $script ) .
'});';
1423 public static function makeInlineScript( $script ) {
1424 $js = self::makeLoaderConditionalScript( $script );
1425 return new WrappedString(
1427 '<script>(window.RLQ=window.RLQ||[]).push(function(){',
1439 public static function makeConfigSetScript(
array $configuration ) {
1455 public static function makePackedModulesString(
$modules ) {
1458 $pos = strrpos( $module,
'.' );
1459 $prefix = $pos ===
false ?
'' : substr( $module, 0, $pos );
1460 $suffix = $pos ===
false ? $module : substr( $module, $pos + 1 );
1461 $groups[$prefix][] = $suffix;
1465 foreach ( $groups
as $prefix => $suffixes ) {
1466 $p = $prefix ===
'' ?
'' : $prefix .
'.';
1467 $arr[] = $p . implode(
',', $suffixes );
1469 $str = implode(
'|', $arr );
1478 public static function inDebugMode() {
1479 if ( self::$debugMode === null ) {
1481 self::$debugMode = $wgRequest->getFuzzyBool(
'debug',
1482 $wgRequest->getCookie(
'resourceLoaderDebug',
'', $wgResourceLoaderDebug )
1485 return self::$debugMode;
1495 public static function clearCache() {
1496 self::$debugMode = null;
1511 $query = self::createLoaderQuery( $context, $extraQuery );
1512 $script = $this->getLoadScript(
$source );
1527 return self::makeLoaderQuery(
1535 $context->
getRequest()->getBool(
'printable' ),
1536 $context->
getRequest()->getBool(
'handheld' ),
1559 $version = null,
$debug =
false, $only = null, $printable =
false,
1560 $handheld =
false, $extraQuery = []
1563 'modules' => self::makePackedModulesString(
$modules ),
1566 'debug' =>
$debug ?
'true' :
'false',
1568 if (
$user !== null ) {
1571 if ( $version !== null ) {
1572 $query[
'version'] = $version;
1574 if ( $only !== null ) {
1599 public static function isValidModuleName( $moduleName ) {
1600 return strcspn( $moduleName,
'!,|', 0, 255 ) === strlen( $moduleName );
1612 public function getLessCompiler( $extraVars = [] ) {
1616 if ( !class_exists(
'Less_Parser' ) ) {
1617 throw new MWException(
'MediaWiki requires the less.php parser' );
1621 $parser->ModifyVars( array_merge( $this->getLessVars(), $extraVars ) );
1623 array_fill_keys( $this->config->get(
'ResourceLoaderLESSImportPaths' ),
'' )
1625 $parser->SetOption(
'relativeUrls',
false );
1636 public function getLessVars() {
1637 if ( !$this->lessVars ) {
1638 $lessVars = $this->config->get(
'ResourceLoaderLESSVars' );
1639 Hooks::run(
'ResourceLoaderGetLessVars', [ &$lessVars ] );
1640 $this->lessVars = $lessVars;
1642 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 "