102 public const CACHE_VERSION = 9;
104 public const FILTER_NOMIN =
'/*@nomin*/';
107 private const RL_DEP_STORE_PREFIX =
'ResourceLoaderModule';
109 private const RL_MODULE_DEP_TTL = BagOStuff::TTL_YEAR;
111 private const MAXAGE_RECOVER = 60;
125 private $hookContainer;
131 private $maxageVersioned;
133 private $maxageUnversioned;
135 private $useFileCache;
140 private $moduleInfos = [];
142 private $testModuleNames = [];
144 private $sources = [];
153 private $depStoreUpdateBuffer = [];
158 private $moduleSkinStyles = [];
185 LoggerInterface $logger =
null,
189 $this->loadScript = $params[
'loadScript'] ??
'/load.php';
190 $this->maxageVersioned = $params[
'maxageVersioned'] ?? 30 * 24 * 60 * 60;
191 $this->maxageUnversioned = $params[
'maxageUnversioned'] ?? 5 * 60;
192 $this->useFileCache = $params[
'useFileCache'] ??
false;
194 $this->config = $config;
195 $this->logger = $logger ?:
new NullLogger();
198 $this->hookContainer = $services->getHookContainer();
199 $this->hookRunner =
new HookRunner( $this->hookContainer );
202 $this->
addSource(
'local', $this->loadScript );
205 $this->
register(
'startup', [
'class' => StartUpModule::class ] );
208 new MessageBlobStore( $this, $this->logger, $services->getMainWANObjectCache() )
219 return $this->config;
227 $this->logger = $logger;
235 return $this->logger;
243 return $this->blobStore;
251 $this->blobStore = $blobStore;
267 $this->moduleSkinStyles = $moduleSkinStyles;
281 public function register( $name, array $info = null ) {
283 $registrations = is_array( $name ) ? $name : [ $name => $info ];
284 foreach ( $registrations as $name => $info ) {
286 if ( isset( $this->moduleInfos[$name] ) ) {
288 $this->logger->warning(
289 'ResourceLoader duplicate registration warning. ' .
290 'Another module has already been registered as ' . $name
295 if ( !self::isValidModuleName( $name ) ) {
296 throw new InvalidArgumentException(
"ResourceLoader module name '$name' is invalid, "
297 .
"see ResourceLoader::isValidModuleName()" );
299 if ( !is_array( $info ) ) {
300 throw new InvalidArgumentException(
301 'Invalid module info for "' . $name .
'": expected array, got ' . gettype( $info )
306 $this->moduleInfos[$name] = $info;
315 $testModulesMeta = [
'qunit' => [] ];
316 $this->hookRunner->onResourceLoaderTestModules( $testModulesMeta, $this );
318 $extRegistry = ExtensionRegistry::getInstance();
320 $testModules = $testModulesMeta[
'qunit']
321 + $extRegistry->getAttribute(
'QUnitTestModules' );
323 $testModuleNames = [];
324 foreach ( $testModules as $name => &$module ) {
326 if ( isset( $module[
'dependencies'] ) && is_string( $module[
'dependencies'] ) ) {
327 $module[
'dependencies'] = [ $module[
'dependencies'] ];
331 $module[
'dependencies'][] =
'mediawiki.qunit-testrunner';
334 $testModuleNames[] = $name;
338 $testModules = ( include MW_INSTALL_PATH .
'/tests/qunit/QUnitTestResources.php' ) + $testModules;
339 $testModuleNames[] =
'test.MediaWiki';
341 $this->
register( $testModules );
342 $this->testModuleNames = $testModuleNames;
355 public function addSource( $sources, $loadUrl =
null ) {
356 if ( !is_array( $sources ) ) {
357 $sources = [ $sources => $loadUrl ];
359 foreach ( $sources as $id =>
$source ) {
361 if ( isset( $this->sources[$id] ) ) {
362 throw new RuntimeException(
'Cannot register source ' . $id .
' twice' );
367 if ( !isset(
$source[
'loadScript'] ) ) {
368 throw new InvalidArgumentException(
'Each source must have a "loadScript" key' );
381 return array_keys( $this->moduleInfos );
392 return $this->testModuleNames;
403 return isset( $this->moduleInfos[$name] );
418 if ( !isset( $this->modules[$name] ) ) {
419 if ( !isset( $this->moduleInfos[$name] ) ) {
424 $info = $this->moduleInfos[$name];
425 if ( isset( $info[
'factory'] ) ) {
427 $object = call_user_func( $info[
'factory'], $info );
429 $class = $info[
'class'] ?? FileModule::class;
431 $object =
new $class( $info );
433 $object->setConfig( $this->getConfig() );
434 $object->setLogger( $this->logger );
435 $object->setHookContainer( $this->hookContainer );
436 $object->setName( $name );
437 $object->setDependencyAccessCallbacks(
438 [ $this,
'loadModuleDependenciesInternal' ],
439 [ $this,
'saveModuleDependenciesInternal' ]
441 $object->setSkinStylesOverride( $this->moduleSkinStyles );
442 $this->modules[$name] = $object;
445 return $this->modules[$name];
457 $entitiesByModule = [];
458 foreach ( $moduleNames as $moduleName ) {
459 $entitiesByModule[$moduleName] =
"$moduleName|$vary";
461 $depsByEntity = $this->depStore->retrieveMulti(
462 self::RL_DEP_STORE_PREFIX,
466 foreach ( $moduleNames as $moduleName ) {
467 $module = $this->getModule( $moduleName );
469 $entity = $entitiesByModule[$moduleName];
470 $deps = $depsByEntity[$entity];
472 $module->setFileDependencies( $context, $paths );
481 $modulesWithMessages = [];
482 foreach ( $moduleNames as $moduleName ) {
483 $module = $this->getModule( $moduleName );
484 if ( $module && $module->getMessages() ) {
485 $modulesWithMessages[$moduleName] = $module;
490 $store = $this->getMessageBlobStore();
491 $blobs = $store->getBlobs( $modulesWithMessages,
$lang );
492 foreach ( $blobs as $moduleName =>
$blob ) {
493 $modulesWithMessages[$moduleName]->setMessageBlob(
$blob,
$lang );
504 $deps = $this->depStore->retrieve( self::RL_DEP_STORE_PREFIX,
"$moduleName|$variant" );
517 $hasPendingUpdate = (bool)$this->depStoreUpdateBuffer;
518 $entity =
"$moduleName|$variant";
520 if ( array_diff( $paths, $priorPaths ) || array_diff( $priorPaths, $paths ) ) {
523 $deps = $this->depStore->newEntityDependencies( $paths, time() );
524 $this->depStoreUpdateBuffer[$entity] = $deps;
526 $this->depStoreUpdateBuffer[$entity] =
null;
536 if ( !$hasPendingUpdate ) {
537 DeferredUpdates::addCallableUpdate(
function () {
538 $updatesByEntity = $this->depStoreUpdateBuffer;
539 $this->depStoreUpdateBuffer = [];
545 foreach ( $updatesByEntity as $entity => $update ) {
546 $lockKey =
$cache->makeKey(
'rl-deps', $entity );
547 $scopeLocks[$entity] =
$cache->getScopedLock( $lockKey, 0 );
548 if ( !$scopeLocks[$entity] ) {
553 if ( $update ===
null ) {
554 $entitiesUnreg[] = $entity;
556 $depsByEntity[$entity] = $update;
560 $ttl = self::RL_MODULE_DEP_TTL;
561 $this->depStore->storeMulti( self::RL_DEP_STORE_PREFIX, $depsByEntity, $ttl );
562 $this->depStore->remove( self::RL_DEP_STORE_PREFIX, $entitiesUnreg );
573 return $this->sources;
585 if ( !isset( $this->sources[
$source] ) ) {
586 throw new UnexpectedValueException(
"Unknown source '$source'" );
588 return $this->sources[
$source];
594 public const HASH_LENGTH = 5;
659 $hash = hash(
'fnv132', $value );
663 \
Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
679 MWExceptionHandler::logException( $e );
680 $this->logger->warning(
682 $context + [
'exception' => $e ]
684 $this->errors[] = self::formatExceptionNoComment( $e );
696 if ( !$moduleNames ) {
699 $hashes = array_map(
function ( $module ) use ( $context ) {
701 return $this->getModule( $module )->getVersionHash( $context );
702 }
catch ( TimeoutException $e ) {
704 }
catch ( Exception $e ) {
707 $this->outputErrorAndLog( $e,
708 'Calculating version for "{module}" failed: {exception}',
716 return self::makeHash( implode(
'',
$hashes ) );
741 if ( !$this->getModule( $name ) ) {
748 return $this->getCombinedVersion( $context, $filtered );
766 $responseTime = $this->measureResponseTime();
772 $module = $this->getModule( $name );
776 if ( $module->getGroup() === Module::GROUP_PRIVATE ) {
778 $this->logger->debug(
"Request for private module '$name' denied" );
779 $this->errors[] =
"Cannot build private module \"$name\"";
790 $this->preloadModuleInfo( array_keys(
$modules ), $context );
791 }
catch ( TimeoutException $e ) {
793 }
catch ( Exception $e ) {
794 $this->outputErrorAndLog( $e,
'Preloading module info failed: {exception}' );
798 $versionHash = $this->getCombinedVersion( $context, array_keys(
$modules ) );
802 $etag =
'W/"' . $versionHash .
'"';
805 if ( $this->tryRespondNotModified( $context, $etag ) ) {
810 if ( $this->useFileCache ) {
812 if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) {
820 $response = $this->makeModuleResponse( $context,
$modules, $missing );
825 $warnings = ob_get_contents();
826 if ( strlen( $warnings ) ) {
827 $this->errors[] = $warnings;
832 if ( $fileCache && !$this->errors && $missing === [] &&
834 if ( $fileCache->isCacheWorthy() ) {
836 $fileCache->saveText( $response );
838 $fileCache->incrMissesRecent( $context->
getRequest() );
842 $this->sendResponseHeaders( $context, $etag, (
bool)$this->errors, $this->extraHeaders );
849 $response = implode(
"\n\n", $this->errors );
850 } elseif ( $this->errors ) {
851 $errorText = implode(
"\n\n", $this->errors );
852 $errorResponse = self::makeComment( $errorText );
854 $errorResponse .=
'if (window.console && console.error) { console.error('
860 $response = $errorResponse . $response;
873 $statStart = $_SERVER[
'REQUEST_TIME_FLOAT'];
874 return new ScopedCallback(
static function () use ( $statStart ) {
875 $statTiming = microtime(
true ) - $statStart;
877 $stats->timing(
'resourceloader.responseTime', $statTiming * 1000 );
892 Context $context, $etag, $errors, array $extra = []
906 $maxage = self::MAXAGE_RECOVER;
907 } elseif ( $context->
getVersion() ===
null ) {
911 $maxage = $this->maxageUnversioned;
915 $maxage = $this->maxageVersioned;
920 header(
'Content-Type: text/plain; charset=utf-8' );
922 $context->
getImageObj()->sendResponseHeaders( $context );
924 } elseif ( $context->
getOnly() ===
'styles' ) {
925 header(
'Content-Type: text/css; charset=utf-8' );
926 header(
'Access-Control-Allow-Origin: *' );
928 header(
'Content-Type: text/javascript; charset=utf-8' );
932 header(
'ETag: ' . $etag );
935 header(
'Cache-Control: private, no-cache, must-revalidate' );
936 header(
'Pragma: no-cache' );
938 header(
"Cache-Control: public, max-age=$maxage, s-maxage=$maxage" );
939 header(
'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
941 foreach ( $extra as
$header ) {
959 $clientKeys = $context->
getRequest()->getHeader(
'If-None-Match', WebRequest::GETHEADER_LIST );
961 if ( $clientKeys !==
false && !$context->
getDebug() && in_array( $etag, $clientKeys ) ) {
973 HttpStatus::header( 304 );
975 $this->sendResponseHeaders( $context, $etag,
false );
998 ? $this->maxageUnversioned
999 : $this->maxageVersioned;
1001 $minTime = time() - $maxage;
1002 $good = $fileCache->
isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1013 $this->sendResponseHeaders( $context, $etag,
false );
1018 $warnings = ob_get_contents();
1019 if ( strlen( $warnings ) ) {
1020 $response = self::makeComment( $warnings ) . $response;
1025 echo $response .
"\n/* Cached {$ts} */";
1043 $encText = str_replace(
'*/',
'* /', $text );
1044 return "/*\n$encText\n*/\n";
1054 return self::makeComment( self::formatExceptionNoComment( $e ) );
1065 if ( !MWExceptionRenderer::shouldShowExceptionDetails() ) {
1066 return MWExceptionHandler::getPublicLogMessage( $e );
1069 return MWExceptionHandler::getLogMessage( $e ) .
1071 MWExceptionHandler::getRedactedTraceAsString( $e );
1086 array
$modules, array $missing = []
1088 if (
$modules === [] && $missing === [] ) {
1098 $data = $image->getImageData( $context );
1099 if ( $data ===
false ) {
1101 $this->errors[] =
'Image generation failed';
1107 foreach ( $missing as $name ) {
1108 $states[$name] =
'missing';
1112 $filter = $only ===
'styles' ?
'minify-css' :
'minify-js';
1116 foreach (
$modules as $name => $module ) {
1118 $content = $module->getModuleContent( $context );
1119 $implementKey = $name .
'@' . $module->getVersionHash( $context );
1122 if ( isset(
$content[
'headers'] ) ) {
1123 $this->extraHeaders = array_merge( $this->extraHeaders,
$content[
'headers'] );
1130 if ( is_string( $scripts ) ) {
1132 $strContent = $scripts;
1133 } elseif ( is_array( $scripts ) ) {
1135 $strContent = self::makeLoaderImplementScript(
1150 $strContent = isset( $styles[
'css'] ) ? implode(
'', $styles[
'css'] ) :
'';
1153 $scripts =
$content[
'scripts'] ??
'';
1154 if ( is_string( $scripts ) ) {
1155 if ( $name ===
'site' || $name ===
'user' ) {
1161 $scripts = self::filter(
'minify-js', $scripts );
1167 $strContent = self::makeLoaderImplementScript(
1181 $strContent = self::ensureNewline( $strContent );
1183 $strContent = self::filter( $filter, $strContent, [
1187 'cache' => !$module->shouldEmbedModule( $context )
1191 if ( $only ===
'scripts' ) {
1193 $out .= self::ensureNewline( $strContent );
1195 $out .= $strContent;
1197 }
catch ( TimeoutException $e ) {
1199 }
catch ( Exception $e ) {
1200 $this->outputErrorAndLog( $e,
'Generating module package failed: {exception}' );
1203 $states[$name] =
'error';
1210 if (
$modules && $only ===
'scripts' ) {
1213 foreach (
$modules as $name => $module ) {
1214 $states[$name] =
'ready';
1220 $stateScript = self::makeLoaderStateScript( $context, $states );
1222 $stateScript = self::filter(
'minify-js', $stateScript );
1225 $out = self::ensureNewline( $out ) . $stateScript;
1227 } elseif ( $states ) {
1228 $this->errors[] =
'Problematic modules: '
1241 public static function ensureNewline( $str ) {
1242 $end = substr( $str, -1 );
1243 if ( $end ===
false || $end ===
'' || $end ===
"\n" ) {
1255 public function getModulesByMessage( $messageKey ) {
1257 foreach ( $this->getModuleNames() as $moduleName ) {
1258 $module = $this->getModule( $moduleName );
1259 if ( in_array( $messageKey, $module->getMessages() ) ) {
1260 $moduleNames[] = $moduleName;
1263 return $moduleNames;
1283 private static function makeLoaderImplementScript(
1284 Context $context, $name, $scripts, $styles, $messages, $templates
1287 if ( $scripts->value ===
'' ) {
1290 $scripts =
new XmlJsCode(
"function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1292 } elseif ( is_array( $scripts ) && isset( $scripts[
'files'] ) ) {
1293 $files = $scripts[
'files'];
1297 if (
$file[
'type'] ===
'script' ) {
1304 $file =
new XmlJsCode(
"function ( require, module, exports ) {\n$content}" );
1310 'main' => $scripts[
'main'],
1311 'files' => XmlJsCode::encodeObject( $files,
true )
1313 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1314 throw new InvalidArgumentException(
'Script must be a string or an array of URLs' );
1327 self::trimArray( $module );
1340 public static function makeMessageSetScript( $messages ) {
1341 return 'mw.messages.set('
1342 . self::encodeJsonForScript( (
object)$messages )
1353 public static function makeCombinedStyles( array $stylePairs ) {
1355 foreach ( $stylePairs as $media => $styles ) {
1358 $styles = (array)$styles;
1359 foreach ( $styles as $style ) {
1360 $style = trim( $style );
1362 if ( $style ===
'' ) {
1369 if ( $media ===
'' || $media ==
'all' ) {
1371 } elseif ( is_string( $media ) ) {
1372 $out[] =
"@media $media {\n" . str_replace(
"\n",
"\n\t",
"\t" . $style ) .
"}";
1389 public static function encodeJsonForScript( $data ) {
1399 $jsonFlags = JSON_UNESCAPED_SLASHES |
1400 JSON_UNESCAPED_UNICODE |
1403 if ( self::inDebugMode() ) {
1404 $jsonFlags |= JSON_PRETTY_PRINT;
1406 return json_encode( $data, $jsonFlags );
1421 public static function makeLoaderStateScript(
1422 Context $context, array $states
1424 return 'mw.loader.state('
1425 . $context->encodeJson( $states )
1429 private static function isEmptyObject( stdClass $obj ) {
1430 foreach ( $obj as $key => $value ) {
1449 private static function trimArray( array &$array ): void {
1450 $i = count( $array );
1452 if ( $array[$i] ===
null
1453 || $array[$i] === []
1454 || ( $array[$i] instanceof
XmlJsCode && $array[$i]->value ===
'{}' )
1455 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1457 unset( $array[$i] );
1489 public static function makeLoaderRegisterScript(
1496 foreach (
$modules as $i => &$module ) {
1498 $index[$module[0]] = $i;
1501 if ( isset( $module[2] ) ) {
1502 foreach ( $module[2] as &$dependency ) {
1503 if ( isset( $index[$dependency] ) ) {
1505 $dependency = $index[$dependency];
1511 array_walk(
$modules, [ self::class,
'trimArray' ] );
1513 return 'mw.loader.register('
1531 public static function makeLoaderSourcesScript(
1532 Context $context, array $sources
1534 return 'mw.loader.addSource('
1535 . $context->encodeJson( $sources )
1545 public static function makeLoaderConditionalScript( $script ) {
1547 return '(RLQ=window.RLQ||[]).push(function(){' .
1548 trim( $script ) .
'});';
1559 public static function makeInlineCodeWithModule(
$modules, $script ) {
1561 return '(RLQ=window.RLQ||[]).push(['
1562 . self::encodeJsonForScript(
$modules ) .
','
1563 .
'function(){' . trim( $script ) .
'}'
1578 public static function makeInlineScript( $script, $nonce =
null ) {
1579 $js = self::makeLoaderConditionalScript( $script );
1581 if ( $nonce ===
null ) {
1582 wfWarn( __METHOD__ .
" did not get nonce. Will break CSP" );
1583 } elseif ( $nonce !==
false ) {
1587 $escNonce =
' nonce="' . htmlspecialchars( $nonce ) .
'"';
1590 return new WrappedString(
1591 Html::inlineScript( $js, $nonce ),
1592 "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1605 public static function makeConfigSetScript( array $configuration ) {
1606 $json = self::encodeJsonForScript( $configuration );
1607 if ( $json ===
false ) {
1609 'JSON serialization of config data failed. ' .
1610 'This usually means the config data is not valid UTF-8.'
1613 return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) .
');';
1615 return "mw.config.set($json);";
1631 public static function makePackedModulesString( array
$modules ) {
1634 $pos = strrpos( $module,
'.' );
1635 $prefix = $pos ===
false ?
'' : substr( $module, 0, $pos );
1636 $suffix = $pos ===
false ? $module : substr( $module, $pos + 1 );
1637 $moduleMap[$prefix][] = $suffix;
1641 foreach ( $moduleMap as $prefix => $suffixes ) {
1642 $p = $prefix ===
'' ?
'' : $prefix .
'.';
1643 $arr[] = $p . implode(
',', $suffixes );
1645 return implode(
'|', $arr );
1659 public static function expandModuleNames(
$modules ) {
1661 $exploded = explode(
'|',
$modules );
1662 foreach ( $exploded as $group ) {
1663 if ( strpos( $group,
',' ) ===
false ) {
1670 $pos = strrpos( $group,
'.' );
1671 if ( $pos ===
false ) {
1673 $retval = array_merge( $retval, explode(
',', $group ) );
1677 $prefix = substr( $group, 0, $pos );
1678 $suffixes = explode(
',', substr( $group, $pos + 1 ) );
1679 foreach ( $suffixes as $suffix ) {
1680 $retval[] =
"$prefix.$suffix";
1696 public static function inDebugMode() {
1697 if ( self::$debugMode ===
null ) {
1699 $resourceLoaderDebug = MediaWikiServices::getInstance()->getMainConfig()->get(
1700 MainConfigNames::ResourceLoaderDebug );
1702 $wgRequest->getCookie(
'resourceLoaderDebug',
'', $resourceLoaderDebug ?
'true' :
'' )
1704 self::$debugMode = Context::debugFromString( $str );
1706 return self::$debugMode;
1719 public static function clearCache() {
1720 self::$debugMode =
null;
1732 public function createLoaderURL(
$source, Context $context,
1733 array $extraQuery = []
1735 $query = self::createLoaderQuery( $context, $extraQuery );
1736 $script = $this->getLoadScript(
$source );
1750 protected static function createLoaderQuery(
1751 Context $context, array $extraQuery = []
1753 return self::makeLoaderQuery(
1754 $context->getModules(),
1755 $context->getLanguage(),
1756 $context->getSkin(),
1757 $context->getUser(),
1758 $context->getVersion(),
1759 $context->getDebug(),
1760 $context->getOnly(),
1761 $context->getRequest()->getBool(
'printable' ),
1783 public static function makeLoaderQuery( array
$modules,
$lang, $skin, $user =
null,
1784 $version =
null,
$debug = Context::DEBUG_OFF, $only =
null,
1785 $printable =
false, $handheld =
null, array $extraQuery = []
1788 'modules' => self::makePackedModulesString(
$modules ),
1794 if (
$lang !== Context::DEFAULT_LANG ) {
1795 $query[
'lang'] =
$lang;
1797 if ( $skin !== Context::DEFAULT_SKIN ) {
1798 $query[
'skin'] = $skin;
1800 if (
$debug !== Context::DEBUG_OFF ) {
1801 $query[
'debug'] = strval(
$debug );
1803 if ( $user !==
null ) {
1804 $query[
'user'] = $user;
1806 if ( $version !==
null ) {
1807 $query[
'version'] = $version;
1809 if ( $only !==
null ) {
1810 $query[
'only'] = $only;
1813 $query[
'printable'] = 1;
1815 $query += $extraQuery;
1831 public static function isValidModuleName( $moduleName ) {
1832 $len = strlen( $moduleName );
1833 return $len <= 255 && strcspn( $moduleName,
'!,|', 0, $len ) === $len;
1847 public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1852 if ( !class_exists( Less_Parser::class ) ) {
1853 throw new MWException(
'MediaWiki requires the less.php parser' );
1856 $importDirs[] =
"$IP/resources/src/mediawiki.less";
1858 $parser =
new Less_Parser;
1859 $parser->ModifyVars( $vars );
1861 $parser->SetImportDirs( array_fill_keys( $importDirs,
'' ) );
1862 $parser->SetOption(
'relativeUrls',
false );
1880 public function expandUrl(
string $base,
string $url ): string {
1882 $isProtoRelative = strpos(
$base,
'//' ) === 0;
1883 if ( $isProtoRelative ) {
1884 $base =
"https:$base";
1887 $baseUrl =
new Net_URL2(
$base );
1888 $ret = $baseUrl->resolve( $url );
1889 if ( $isProtoRelative ) {
1890 $ret->setScheme(
false );
1892 return $ret->getURL();
1912 public static function filter( $filter, $data, array $options = [] ) {
1913 if ( strpos( $data, self::FILTER_NOMIN ) !==
false ) {
1917 if ( isset( $options[
'cache'] ) && $options[
'cache'] ===
false ) {
1918 return self::applyFilter( $filter, $data ) ?? $data;
1921 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1924 $key =
$cache->makeGlobalKey(
1925 'resourceloader-filter',
1927 self::CACHE_VERSION,
1931 $incKey =
"resourceloader_cache.$filter.hit";
1932 $result =
$cache->getWithSetCallback(
1935 function () use ( $filter, $data, &$incKey ) {
1936 $incKey =
"resourceloader_cache.$filter.miss";
1937 return self::applyFilter( $filter, $data );
1940 $stats->increment( $incKey );
1941 if ( $result ===
null ) {
1954 private static function applyFilter( $filter, $data ) {
1955 $data = trim( $data );
1958 $data = ( $filter ===
'minify-css' )
1959 ? CSSMin::minify( $data )
1960 : JavaScriptMinifier::minify( $data );
1961 }
catch ( TimeoutException $e ) {
1963 }
catch ( Exception $e ) {
1982 public static function getUserDefaults(
1984 HookContainer $hookContainer,
1985 UserOptionsLookup $userOptionsLookup
1987 $defaultOptions = $userOptionsLookup->getDefaultOptions();
1988 $keysToExclude = [];
1989 $hookRunner =
new HookRunner( $hookContainer );
1990 $hookRunner->onResourceLoaderExcludeUserOptions( $keysToExclude, $context );
1991 foreach ( $keysToExclude as $excludedKey ) {
1992 unset( $defaultOptions[ $excludedKey ] );
1994 return $defaultOptions;
2005 public static function getSiteConfigSettings(
2006 Context $context,
Config $conf
2011 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2012 $namespaceIds = $contLang->getNamespaceIds();
2013 $caseSensitiveNamespaces = [];
2014 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2015 foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
2016 $namespaceIds[$contLang->lc( $name )] = $index;
2017 if ( !$nsInfo->isCapitalized( $index ) ) {
2018 $caseSensitiveNamespaces[] = $index;
2022 $illegalFileChars = $conf->
get( MainConfigNames::IllegalFileChars );
2025 $skin = $context->getSkin();
2029 'debug' => $context->getDebug(),
2031 'stylepath' => $conf->
get( MainConfigNames::StylePath ),
2032 'wgArticlePath' => $conf->
get( MainConfigNames::ArticlePath ),
2033 'wgScriptPath' => $conf->
get( MainConfigNames::ScriptPath ),
2034 'wgScript' => $conf->
get( MainConfigNames::Script ),
2035 'wgSearchType' => $conf->
get( MainConfigNames::SearchType ),
2036 'wgVariantArticlePath' => $conf->
get( MainConfigNames::VariantArticlePath ),
2037 'wgServer' => $conf->
get( MainConfigNames::Server ),
2038 'wgServerName' => $conf->
get( MainConfigNames::ServerName ),
2039 'wgUserLanguage' => $context->getLanguage(),
2040 'wgContentLanguage' => $contLang->getCode(),
2042 'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
2043 'wgNamespaceIds' => $namespaceIds,
2044 'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
2045 'wgSiteName' => $conf->
get( MainConfigNames::Sitename ),
2046 'wgDBname' => $conf->
get( MainConfigNames::DBname ),
2048 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
2050 'wgExtensionAssetsPath' => $conf->
get( MainConfigNames::ExtensionAssetsPath ),
2061 'wgActionPaths' => (object)$conf->
get( MainConfigNames::ActionPaths ),
2063 'wgTranslateNumerals' => $conf->
get( MainConfigNames::TranslateNumerals ),
2065 'wgExtraSignatureNamespaces' => $conf->
get( MainConfigNames::ExtraSignatureNamespaces ),
2070 Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );