93 public const CACHE_VERSION = 9;
95 public const FILTER_NOMIN =
'/*@nomin*/';
98 private const RL_DEP_STORE_PREFIX =
'ResourceLoaderModule';
100 private const RL_MODULE_DEP_TTL = BagOStuff::TTL_YEAR;
102 private const MAXAGE_RECOVER = 60;
116 private $hookContainer;
122 private $maxageVersioned;
124 private $maxageUnversioned;
126 private $useFileCache;
131 private $moduleInfos = [];
133 private $testModuleNames = [];
135 private $sources = [];
144 private $depStoreUpdateBuffer = [];
149 private $moduleSkinStyles = [];
176 LoggerInterface $logger =
null,
180 $this->loadScript = $params[
'loadScript'] ??
'/load.php';
181 $this->maxageVersioned = $params[
'maxageVersioned'] ?? 30 * 24 * 60 * 60;
182 $this->maxageUnversioned = $params[
'maxageUnversioned'] ?? 5 * 60;
183 $this->useFileCache = $params[
'useFileCache'] ??
false;
185 $this->config = $config;
186 $this->logger = $logger ?:
new NullLogger();
189 $this->hookContainer = $services->getHookContainer();
190 $this->hookRunner =
new HookRunner( $this->hookContainer );
193 $this->
addSource(
'local', $this->loadScript );
196 $this->
register(
'startup', [
'class' => StartUpModule::class ] );
199 new MessageBlobStore( $this, $this->logger, $services->getMainWANObjectCache() )
210 return $this->config;
218 $this->logger = $logger;
226 return $this->logger;
234 return $this->blobStore;
242 $this->blobStore = $blobStore;
258 $this->moduleSkinStyles = $moduleSkinStyles;
272 public function register( $name, array $info = null ) {
274 $registrations = is_array( $name ) ? $name : [ $name => $info ];
275 foreach ( $registrations as $name => $info ) {
277 if ( isset( $this->moduleInfos[$name] ) ) {
279 $this->logger->warning(
280 'ResourceLoader duplicate registration warning. ' .
281 'Another module has already been registered as ' . $name
286 if ( !self::isValidModuleName( $name ) ) {
287 throw new InvalidArgumentException(
"ResourceLoader module name '$name' is invalid, "
288 .
"see ResourceLoader::isValidModuleName()" );
290 if ( !is_array( $info ) ) {
291 throw new InvalidArgumentException(
292 'Invalid module info for "' . $name .
'": expected array, got ' . gettype( $info )
297 $this->moduleInfos[$name] = $info;
307 $testModules = $extRegistry->
getAttribute(
'QUnitTestModules' );
309 $testModuleNames = [];
310 foreach ( $testModules as $name => &$module ) {
312 if ( isset( $module[
'dependencies'] ) && is_string( $module[
'dependencies'] ) ) {
313 $module[
'dependencies'] = [ $module[
'dependencies'] ];
317 $module[
'dependencies'][] =
'mediawiki.qunit-testrunner';
320 $testModuleNames[] = $name;
324 $testModules = ( include MW_INSTALL_PATH .
'/tests/qunit/QUnitTestResources.php' ) + $testModules;
325 $testModuleNames[] =
'test.MediaWiki';
327 $this->
register( $testModules );
328 $this->testModuleNames = $testModuleNames;
341 public function addSource( $sources, $loadUrl =
null ) {
342 if ( !is_array( $sources ) ) {
343 $sources = [ $sources => $loadUrl ];
345 foreach ( $sources as $id =>
$source ) {
347 if ( isset( $this->sources[$id] ) ) {
348 throw new RuntimeException(
'Cannot register source ' . $id .
' twice' );
353 if ( !isset(
$source[
'loadScript'] ) ) {
354 throw new InvalidArgumentException(
'Each source must have a "loadScript" key' );
367 return array_keys( $this->moduleInfos );
378 return $this->testModuleNames;
389 return isset( $this->moduleInfos[$name] );
404 if ( !isset( $this->modules[$name] ) ) {
405 if ( !isset( $this->moduleInfos[$name] ) ) {
410 $info = $this->moduleInfos[$name];
411 if ( isset( $info[
'factory'] ) ) {
413 $object = call_user_func( $info[
'factory'], $info );
415 $class = $info[
'class'] ?? FileModule::class;
417 $object =
new $class( $info );
419 $object->setConfig( $this->getConfig() );
420 $object->setLogger( $this->logger );
421 $object->setHookContainer( $this->hookContainer );
422 $object->setName( $name );
423 $object->setDependencyAccessCallbacks(
424 [ $this,
'loadModuleDependenciesInternal' ],
425 [ $this,
'saveModuleDependenciesInternal' ]
427 $object->setSkinStylesOverride( $this->moduleSkinStyles );
428 $this->modules[$name] = $object;
431 return $this->modules[$name];
443 $entitiesByModule = [];
444 foreach ( $moduleNames as $moduleName ) {
445 $entitiesByModule[$moduleName] =
"$moduleName|$vary";
447 $depsByEntity = $this->depStore->retrieveMulti(
448 self::RL_DEP_STORE_PREFIX,
452 foreach ( $moduleNames as $moduleName ) {
453 $module = $this->getModule( $moduleName );
455 $entity = $entitiesByModule[$moduleName];
456 $deps = $depsByEntity[$entity];
458 $module->setFileDependencies( $context, $paths );
467 $modulesWithMessages = [];
468 foreach ( $moduleNames as $moduleName ) {
469 $module = $this->getModule( $moduleName );
470 if ( $module && $module->getMessages() ) {
471 $modulesWithMessages[$moduleName] = $module;
476 $store = $this->getMessageBlobStore();
477 $blobs = $store->getBlobs( $modulesWithMessages,
$lang );
478 foreach ( $blobs as $moduleName =>
$blob ) {
479 $modulesWithMessages[$moduleName]->setMessageBlob(
$blob,
$lang );
490 $deps = $this->depStore->retrieve( self::RL_DEP_STORE_PREFIX,
"$moduleName|$variant" );
503 $hasPendingUpdate = (bool)$this->depStoreUpdateBuffer;
504 $entity =
"$moduleName|$variant";
506 if ( array_diff( $paths, $priorPaths ) || array_diff( $priorPaths, $paths ) ) {
509 $deps = $this->depStore->newEntityDependencies( $paths, time() );
510 $this->depStoreUpdateBuffer[$entity] = $deps;
512 $this->depStoreUpdateBuffer[$entity] =
null;
522 if ( !$hasPendingUpdate ) {
523 DeferredUpdates::addCallableUpdate(
function () {
524 $updatesByEntity = $this->depStoreUpdateBuffer;
525 $this->depStoreUpdateBuffer = [];
526 $cache = ObjectCache::getLocalClusterInstance();
531 foreach ( $updatesByEntity as $entity => $update ) {
532 $lockKey = $cache->makeKey(
'rl-deps', $entity );
533 $scopeLocks[$entity] = $cache->getScopedLock( $lockKey, 0 );
534 if ( !$scopeLocks[$entity] ) {
539 if ( $update ===
null ) {
540 $entitiesUnreg[] = $entity;
542 $depsByEntity[$entity] = $update;
546 $ttl = self::RL_MODULE_DEP_TTL;
547 $this->depStore->storeMulti( self::RL_DEP_STORE_PREFIX, $depsByEntity, $ttl );
548 $this->depStore->remove( self::RL_DEP_STORE_PREFIX, $entitiesUnreg );
559 return $this->sources;
571 if ( !isset( $this->sources[
$source] ) ) {
572 throw new UnexpectedValueException(
"Unknown source '$source'" );
574 return $this->sources[
$source];
580 public const HASH_LENGTH = 5;
645 $hash = hash(
'fnv132', $value );
649 \
Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
665 MWExceptionHandler::logException( $e );
666 $this->logger->warning(
668 $context + [
'exception' => $e ]
670 $this->errors[] = self::formatExceptionNoComment( $e );
682 if ( !$moduleNames ) {
685 $hashes = array_map(
function ( $module ) use ( $context ) {
687 return $this->getModule( $module )->getVersionHash( $context );
688 }
catch ( TimeoutException $e ) {
690 }
catch ( Exception $e ) {
693 $this->outputErrorAndLog( $e,
694 'Calculating version for "{module}" failed: {exception}',
702 return self::makeHash( implode(
'',
$hashes ) );
727 if ( !$this->getModule( $name ) ) {
734 return $this->getCombinedVersion( $context, $filtered );
752 $responseTime = $this->measureResponseTime();
758 $module = $this->getModule( $name );
762 if ( $module->getGroup() === Module::GROUP_PRIVATE ) {
764 $this->logger->debug(
"Request for private module '$name' denied" );
765 $this->errors[] =
"Cannot build private module \"$name\"";
776 $this->preloadModuleInfo( array_keys(
$modules ), $context );
777 }
catch ( TimeoutException $e ) {
779 }
catch ( Exception $e ) {
780 $this->outputErrorAndLog( $e,
'Preloading module info failed: {exception}' );
784 $versionHash = $this->getCombinedVersion( $context, array_keys(
$modules ) );
788 $etag =
'W/"' . $versionHash .
'"';
791 if ( $this->tryRespondNotModified( $context, $etag ) ) {
796 if ( $this->useFileCache ) {
798 if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
806 $response = $this->makeModuleResponse( $context,
$modules, $missing );
811 $warnings = ob_get_contents();
812 if ( strlen( $warnings ) ) {
813 $this->errors[] = $warnings;
818 if ( $fileCache && !$this->errors && $missing === [] &&
820 if ( $fileCache->isCacheWorthy() ) {
822 $fileCache->saveText( $response );
824 $fileCache->incrMissesRecent( $context->
getRequest() );
828 $this->sendResponseHeaders( $context, $etag, (
bool)$this->errors, $this->extraHeaders );
835 $response = implode(
"\n\n", $this->errors );
836 } elseif ( $this->errors ) {
837 $errorText = implode(
"\n\n", $this->errors );
838 $errorResponse = self::makeComment( $errorText );
840 $errorResponse .=
'if (window.console && console.error) { console.error('
846 $response = $errorResponse . $response;
859 $statStart = $_SERVER[
'REQUEST_TIME_FLOAT'];
860 return new ScopedCallback(
static function () use ( $statStart ) {
861 $statTiming = microtime(
true ) - $statStart;
863 $stats->timing(
'resourceloader.responseTime', $statTiming * 1000 );
878 Context $context, $etag, $errors, array $extra = []
892 $maxage = self::MAXAGE_RECOVER;
893 } elseif ( $context->
getVersion() ===
null ) {
897 $maxage = $this->maxageUnversioned;
901 $maxage = $this->maxageVersioned;
906 header(
'Content-Type: text/plain; charset=utf-8' );
908 $context->
getImageObj()->sendResponseHeaders( $context );
910 } elseif ( $context->
getOnly() ===
'styles' ) {
911 header(
'Content-Type: text/css; charset=utf-8' );
912 header(
'Access-Control-Allow-Origin: *' );
914 header(
'Content-Type: text/javascript; charset=utf-8' );
918 header(
'ETag: ' . $etag );
921 header(
'Cache-Control: private, no-cache, must-revalidate' );
922 header(
'Pragma: no-cache' );
926 $staleDirective = ( $maxage > self::MAXAGE_RECOVER
927 ?
", stale-while-revalidate=" . min( 60, intval( $maxage / 2 ) )
930 header(
"Cache-Control: public, max-age=$maxage, s-maxage=$maxage" . $staleDirective );
931 header(
'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
933 foreach ( $extra as
$header ) {
951 $clientKeys = $context->
getRequest()->getHeader(
'If-None-Match', WebRequest::GETHEADER_LIST );
953 if ( $clientKeys !==
false && !$context->
getDebug() && in_array( $etag, $clientKeys ) ) {
965 HttpStatus::header( 304 );
967 $this->sendResponseHeaders( $context, $etag,
false );
990 ? $this->maxageUnversioned
991 : $this->maxageVersioned;
993 $minTime = time() - $maxage;
994 $good = $fileCache->
isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1005 $this->sendResponseHeaders( $context, $etag,
false );
1010 $warnings = ob_get_contents();
1011 if ( strlen( $warnings ) ) {
1012 $response = self::makeComment( $warnings ) . $response;
1017 echo $response .
"\n/* Cached {$ts} */";
1035 $encText = str_replace(
'*/',
'* /', $text );
1036 return "/*\n$encText\n*/\n";
1046 return self::makeComment( self::formatExceptionNoComment( $e ) );
1057 if ( !MWExceptionRenderer::shouldShowExceptionDetails() ) {
1058 return MWExceptionHandler::getPublicLogMessage( $e );
1061 return MWExceptionHandler::getLogMessage( $e ) .
1063 MWExceptionHandler::getRedactedTraceAsString( $e );
1078 array
$modules, array $missing = []
1080 if (
$modules === [] && $missing === [] ) {
1090 $data = $image->getImageData( $context );
1091 if ( $data ===
false ) {
1093 $this->errors[] =
'Image generation failed';
1099 foreach ( $missing as $name ) {
1100 $states[$name] =
'missing';
1104 $filter = $only ===
'styles' ?
'minify-css' :
'minify-js';
1105 $debug = (bool)$context->
getDebug();
1108 foreach (
$modules as $name => $module ) {
1110 $content = $module->getModuleContent( $context );
1111 $implementKey = $name .
'@' . $module->getVersionHash( $context );
1114 if ( isset(
$content[
'headers'] ) ) {
1115 $this->extraHeaders = array_merge( $this->extraHeaders,
$content[
'headers'] );
1122 if ( is_string( $scripts ) ) {
1124 $strContent = $scripts;
1125 } elseif ( is_array( $scripts ) ) {
1127 $strContent = self::makeLoaderImplementScript(
1141 $strContent = isset( $styles[
'css'] ) ? implode(
'', $styles[
'css'] ) :
'';
1144 $scripts =
$content[
'scripts'] ??
'';
1145 if ( is_string( $scripts ) ) {
1146 if ( $name ===
'site' || $name ===
'user' ) {
1152 $scripts = self::filter(
'minify-js', $scripts );
1158 $strContent = self::makeLoaderImplementScript(
1171 $strContent = self::ensureNewline( $strContent );
1173 $strContent = self::filter( $filter, $strContent, [
1177 'cache' => !$module->shouldEmbedModule( $context )
1181 if ( $only ===
'scripts' ) {
1183 $out .= self::ensureNewline( $strContent );
1185 $out .= $strContent;
1187 }
catch ( TimeoutException $e ) {
1189 }
catch ( Exception $e ) {
1190 $this->outputErrorAndLog( $e,
'Generating module package failed: {exception}' );
1193 $states[$name] =
'error';
1200 if (
$modules && $only ===
'scripts' ) {
1203 foreach (
$modules as $name => $module ) {
1204 $states[$name] =
'ready';
1210 $stateScript = self::makeLoaderStateScript( $context, $states );
1212 $stateScript = self::filter(
'minify-js', $stateScript );
1215 $out = self::ensureNewline( $out ) . $stateScript;
1217 } elseif ( $states ) {
1218 $this->errors[] =
'Problematic modules: '
1235 public static function ensureNewline( $str ) {
1236 $end = substr( $str, -1 );
1237 if ( $end ===
false || $end ===
'' || $end ===
"\n" ) {
1249 public function getModulesByMessage( $messageKey ) {
1251 foreach ( $this->getModuleNames() as $moduleName ) {
1252 $module = $this->getModule( $moduleName );
1253 if ( in_array( $messageKey, $module->getMessages() ) ) {
1254 $moduleNames[] = $moduleName;
1257 return $moduleNames;
1276 private static function makeLoaderImplementScript(
1277 $name, $scripts, $styles, $messages, $templates
1280 if ( $scripts->value ===
'' ) {
1283 $scripts =
new XmlJsCode(
"function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1285 } elseif ( is_array( $scripts ) && isset( $scripts[
'files'] ) ) {
1286 $files = $scripts[
'files'];
1287 foreach ( $files as &
$file ) {
1290 if (
$file[
'type'] ===
'script' ) {
1297 $file =
new XmlJsCode(
"function ( require, module, exports ) {\n$content}" );
1303 'main' => $scripts[
'main'],
1304 'files' => XmlJsCode::encodeObject( $files,
true )
1306 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1307 throw new InvalidArgumentException(
'Script must be a string or an array of URLs' );
1320 self::trimArray( $module );
1334 public static function makeCombinedStyles( array $stylePairs ) {
1336 foreach ( $stylePairs as $media => $styles ) {
1339 $styles = (array)$styles;
1340 foreach ( $styles as $style ) {
1341 $style = trim( $style );
1343 if ( $style ===
'' ) {
1350 if ( $media ===
'' || $media ==
'all' ) {
1352 } elseif ( is_string( $media ) ) {
1353 $out[] =
"@media $media {\n" . str_replace(
"\n",
"\n\t",
"\t" . $style ) .
"}";
1368 private static function encodeJsonForScript( $data ) {
1378 $jsonFlags = JSON_UNESCAPED_SLASHES |
1379 JSON_UNESCAPED_UNICODE |
1382 if ( self::inDebugMode() ) {
1383 $jsonFlags |= JSON_PRETTY_PRINT;
1385 return json_encode( $data, $jsonFlags );
1400 public static function makeLoaderStateScript(
1401 Context $context, array $states
1403 return 'mw.loader.state('
1404 . $context->encodeJson( $states )
1408 private static function isEmptyObject( stdClass $obj ) {
1409 foreach ( $obj as $key => $value ) {
1428 private static function trimArray( array &$array ): void {
1429 $i = count( $array );
1431 if ( $array[$i] ===
null
1432 || $array[$i] === []
1433 || ( $array[$i] instanceof
XmlJsCode && $array[$i]->value ===
'{}' )
1434 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1436 unset( $array[$i] );
1468 public static function makeLoaderRegisterScript(
1475 foreach (
$modules as $i => &$module ) {
1477 $index[$module[0]] = $i;
1480 if ( isset( $module[2] ) ) {
1481 foreach ( $module[2] as &$dependency ) {
1482 if ( isset( $index[$dependency] ) ) {
1484 $dependency = $index[$dependency];
1490 array_walk(
$modules, [ self::class,
'trimArray' ] );
1492 return 'mw.loader.register('
1510 public static function makeLoaderSourcesScript(
1511 Context $context, array $sources
1513 return 'mw.loader.addSource('
1514 . $context->encodeJson( $sources )
1524 public static function makeLoaderConditionalScript( $script ) {
1526 return '(RLQ=window.RLQ||[]).push(function(){' .
1527 trim( $script ) .
'});';
1538 public static function makeInlineCodeWithModule(
$modules, $script ) {
1540 return '(RLQ=window.RLQ||[]).push(['
1541 . self::encodeJsonForScript(
$modules ) .
','
1542 .
'function(){' . trim( $script ) .
'}'
1557 public static function makeInlineScript( $script, $nonce =
null ) {
1558 $js = self::makeLoaderConditionalScript( $script );
1560 if ( $nonce ===
null ) {
1561 wfWarn( __METHOD__ .
" did not get nonce. Will break CSP" );
1562 } elseif ( $nonce !==
false ) {
1566 $escNonce =
' nonce="' . htmlspecialchars( $nonce ) .
'"';
1569 return new WrappedString(
1570 Html::inlineScript( $js, $nonce ),
1571 "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1584 public static function makeConfigSetScript( array $configuration ) {
1585 $json = self::encodeJsonForScript( $configuration );
1586 if ( $json ===
false ) {
1588 'JSON serialization of config data failed. ' .
1589 'This usually means the config data is not valid UTF-8.'
1592 return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) .
');';
1594 return "mw.config.set($json);";
1610 public static function makePackedModulesString( array
$modules ) {
1613 $pos = strrpos( $module,
'.' );
1614 $prefix = $pos ===
false ?
'' : substr( $module, 0, $pos );
1615 $suffix = $pos ===
false ? $module : substr( $module, $pos + 1 );
1616 $moduleMap[$prefix][] = $suffix;
1620 foreach ( $moduleMap as $prefix => $suffixes ) {
1621 $p = $prefix ===
'' ?
'' : $prefix .
'.';
1622 $arr[] = $p . implode(
',', $suffixes );
1624 return implode(
'|', $arr );
1638 public static function expandModuleNames(
$modules ) {
1640 $exploded = explode(
'|',
$modules );
1641 foreach ( $exploded as $group ) {
1642 if ( strpos( $group,
',' ) ===
false ) {
1649 $pos = strrpos( $group,
'.' );
1650 if ( $pos ===
false ) {
1652 $retval = array_merge( $retval, explode(
',', $group ) );
1656 $prefix = substr( $group, 0, $pos );
1657 $suffixes = explode(
',', substr( $group, $pos + 1 ) );
1658 foreach ( $suffixes as $suffix ) {
1659 $retval[] =
"$prefix.$suffix";
1675 public static function inDebugMode() {
1676 if ( self::$debugMode ===
null ) {
1678 $resourceLoaderDebug = MediaWikiServices::getInstance()->getMainConfig()->get(
1679 MainConfigNames::ResourceLoaderDebug );
1681 $wgRequest->getCookie(
'resourceLoaderDebug',
'', $resourceLoaderDebug ?
'true' :
'' )
1683 self::$debugMode = Context::debugFromString( $str );
1685 return self::$debugMode;
1698 public static function clearCache() {
1699 self::$debugMode =
null;
1711 public function createLoaderURL(
$source, Context $context,
1712 array $extraQuery = []
1714 $query = self::createLoaderQuery( $context, $extraQuery );
1715 $script = $this->getLoadScript(
$source );
1729 protected static function createLoaderQuery(
1730 Context $context, array $extraQuery = []
1732 return self::makeLoaderQuery(
1733 $context->getModules(),
1734 $context->getLanguage(),
1735 $context->getSkin(),
1736 $context->getUser(),
1737 $context->getVersion(),
1738 $context->getDebug(),
1739 $context->getOnly(),
1740 $context->getRequest()->getBool(
'printable' ),
1762 public static function makeLoaderQuery( array
$modules,
$lang, $skin, $user =
null,
1763 $version =
null, $debug = Context::DEBUG_OFF, $only =
null,
1764 $printable =
false, $handheld =
null, array $extraQuery = []
1767 'modules' => self::makePackedModulesString(
$modules ),
1773 if (
$lang !== Context::DEFAULT_LANG ) {
1774 $query[
'lang'] =
$lang;
1776 if ( $skin !== Context::DEFAULT_SKIN ) {
1777 $query[
'skin'] = $skin;
1779 if ( $debug !== Context::DEBUG_OFF ) {
1780 $query[
'debug'] = strval( $debug );
1782 if ( $user !==
null ) {
1783 $query[
'user'] = $user;
1785 if ( $version !==
null ) {
1786 $query[
'version'] = $version;
1788 if ( $only !==
null ) {
1789 $query[
'only'] = $only;
1792 $query[
'printable'] = 1;
1794 $query += $extraQuery;
1810 public static function isValidModuleName( $moduleName ) {
1811 $len = strlen( $moduleName );
1812 return $len <= 255 && strcspn( $moduleName,
'!,|', 0, $len ) === $len;
1826 public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1831 if ( !class_exists( Less_Parser::class ) ) {
1832 throw new MWException(
'MediaWiki requires the less.php parser' );
1835 $importDirs[] =
"$IP/resources/src/mediawiki.less";
1837 $parser =
new Less_Parser;
1838 $parser->ModifyVars( $vars );
1840 $parser->SetImportDirs( array_fill_keys( $importDirs,
'' ) );
1841 $parser->SetOption(
'relativeUrls',
false );
1859 public function expandUrl(
string $base,
string $url ): string {
1861 $isProtoRelative = strpos(
$base,
'//' ) === 0;
1862 if ( $isProtoRelative ) {
1863 $base =
"https:$base";
1866 $baseUrl =
new Net_URL2(
$base );
1867 $ret = $baseUrl->resolve( $url );
1868 if ( $isProtoRelative ) {
1869 $ret->setScheme(
false );
1871 return $ret->getURL();
1891 public static function filter( $filter, $data, array $options = [] ) {
1892 if ( strpos( $data, self::FILTER_NOMIN ) !==
false ) {
1896 if ( isset( $options[
'cache'] ) && $options[
'cache'] ===
false ) {
1897 return self::applyFilter( $filter, $data ) ?? $data;
1900 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1903 $key = $cache->makeGlobalKey(
1904 'resourceloader-filter',
1906 self::CACHE_VERSION,
1910 $incKey =
"resourceloader_cache.$filter.hit";
1911 $result = $cache->getWithSetCallback(
1914 function () use ( $filter, $data, &$incKey ) {
1915 $incKey =
"resourceloader_cache.$filter.miss";
1916 return self::applyFilter( $filter, $data );
1919 $stats->increment( $incKey );
1922 return $result ?? $data;
1930 private static function applyFilter( $filter, $data ) {
1931 $data = trim( $data );
1934 $data = ( $filter ===
'minify-css' )
1935 ? CSSMin::minify( $data )
1936 : JavaScriptMinifier::minify( $data );
1937 }
catch ( TimeoutException $e ) {
1939 }
catch ( Exception $e ) {
1958 public static function getUserDefaults(
1960 HookContainer $hookContainer,
1961 UserOptionsLookup $userOptionsLookup
1963 $defaultOptions = $userOptionsLookup->getDefaultOptions();
1964 $keysToExclude = [];
1965 $hookRunner =
new HookRunner( $hookContainer );
1966 $hookRunner->onResourceLoaderExcludeUserOptions( $keysToExclude, $context );
1967 foreach ( $keysToExclude as $excludedKey ) {
1968 unset( $defaultOptions[ $excludedKey ] );
1970 return $defaultOptions;
1981 public static function getSiteConfigSettings(
1982 Context $context,
Config $conf
1987 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
1988 $namespaceIds = $contLang->getNamespaceIds();
1989 $caseSensitiveNamespaces = [];
1990 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1991 foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
1992 $namespaceIds[$contLang->lc( $name )] = $index;
1993 if ( !$nsInfo->isCapitalized( $index ) ) {
1994 $caseSensitiveNamespaces[] = $index;
1998 $illegalFileChars = $conf->
get( MainConfigNames::IllegalFileChars );
2001 $skin = $context->getSkin();
2005 'debug' => $context->getDebug(),
2007 'stylepath' => $conf->
get( MainConfigNames::StylePath ),
2008 'wgArticlePath' => $conf->
get( MainConfigNames::ArticlePath ),
2009 'wgScriptPath' => $conf->
get( MainConfigNames::ScriptPath ),
2010 'wgScript' => $conf->
get( MainConfigNames::Script ),
2011 'wgSearchType' => $conf->
get( MainConfigNames::SearchType ),
2012 'wgVariantArticlePath' => $conf->
get( MainConfigNames::VariantArticlePath ),
2013 'wgServer' => $conf->
get( MainConfigNames::Server ),
2014 'wgServerName' => $conf->
get( MainConfigNames::ServerName ),
2015 'wgUserLanguage' => $context->getLanguage(),
2016 'wgContentLanguage' => $contLang->getCode(),
2018 'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
2019 'wgNamespaceIds' => $namespaceIds,
2020 'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
2021 'wgSiteName' => $conf->
get( MainConfigNames::Sitename ),
2022 'wgDBname' => $conf->
get( MainConfigNames::DBname ),
2023 'wgWikiID' => WikiMap::getCurrentWikiId(),
2024 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
2025 'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
2026 'wgExtensionAssetsPath' => $conf->
get( MainConfigNames::ExtensionAssetsPath ),
2037 'wgActionPaths' => (object)$conf->
get( MainConfigNames::ActionPaths ),
2039 'wgTranslateNumerals' => $conf->
get( MainConfigNames::TranslateNumerals ),
2041 'wgExtraSignatureNamespaces' => $conf->
get( MainConfigNames::ExtraSignatureNamespaces ),
2042 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
2043 'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
2046 Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );