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' );
940 $staleDirective = ( $maxage > self::MAXAGE_RECOVER
941 ?
", stale-while-revalidate=" . min( 60, intval( $maxage / 2 ) )
944 header(
"Cache-Control: public, max-age=$maxage, s-maxage=$maxage" . $staleDirective );
945 header(
'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
947 foreach ( $extra as
$header ) {
965 $clientKeys = $context->
getRequest()->getHeader(
'If-None-Match', WebRequest::GETHEADER_LIST );
967 if ( $clientKeys !==
false && !$context->
getDebug() && in_array( $etag, $clientKeys ) ) {
979 HttpStatus::header( 304 );
981 $this->sendResponseHeaders( $context, $etag,
false );
1004 ? $this->maxageUnversioned
1005 : $this->maxageVersioned;
1007 $minTime = time() - $maxage;
1008 $good = $fileCache->
isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1019 $this->sendResponseHeaders( $context, $etag,
false );
1024 $warnings = ob_get_contents();
1025 if ( strlen( $warnings ) ) {
1026 $response = self::makeComment( $warnings ) . $response;
1031 echo $response .
"\n/* Cached {$ts} */";
1049 $encText = str_replace(
'*/',
'* /', $text );
1050 return "/*\n$encText\n*/\n";
1060 return self::makeComment( self::formatExceptionNoComment( $e ) );
1071 if ( !MWExceptionRenderer::shouldShowExceptionDetails() ) {
1072 return MWExceptionHandler::getPublicLogMessage( $e );
1075 return MWExceptionHandler::getLogMessage( $e ) .
1077 MWExceptionHandler::getRedactedTraceAsString( $e );
1092 array
$modules, array $missing = []
1094 if (
$modules === [] && $missing === [] ) {
1104 $data = $image->getImageData( $context );
1105 if ( $data ===
false ) {
1107 $this->errors[] =
'Image generation failed';
1113 foreach ( $missing as $name ) {
1114 $states[$name] =
'missing';
1118 $filter = $only ===
'styles' ?
'minify-css' :
'minify-js';
1122 foreach (
$modules as $name => $module ) {
1124 $content = $module->getModuleContent( $context );
1125 $implementKey = $name .
'@' . $module->getVersionHash( $context );
1128 if ( isset(
$content[
'headers'] ) ) {
1129 $this->extraHeaders = array_merge( $this->extraHeaders,
$content[
'headers'] );
1136 if ( is_string( $scripts ) ) {
1138 $strContent = $scripts;
1139 } elseif ( is_array( $scripts ) ) {
1141 $strContent = self::makeLoaderImplementScript(
1156 $strContent = isset( $styles[
'css'] ) ? implode(
'', $styles[
'css'] ) :
'';
1159 $scripts =
$content[
'scripts'] ??
'';
1160 if ( is_string( $scripts ) ) {
1161 if ( $name ===
'site' || $name ===
'user' ) {
1167 $scripts = self::filter(
'minify-js', $scripts );
1173 $strContent = self::makeLoaderImplementScript(
1187 $strContent = self::ensureNewline( $strContent );
1189 $strContent = self::filter( $filter, $strContent, [
1193 'cache' => !$module->shouldEmbedModule( $context )
1197 if ( $only ===
'scripts' ) {
1199 $out .= self::ensureNewline( $strContent );
1201 $out .= $strContent;
1203 }
catch ( TimeoutException $e ) {
1205 }
catch ( Exception $e ) {
1206 $this->outputErrorAndLog( $e,
'Generating module package failed: {exception}' );
1209 $states[$name] =
'error';
1216 if (
$modules && $only ===
'scripts' ) {
1219 foreach (
$modules as $name => $module ) {
1220 $states[$name] =
'ready';
1226 $stateScript = self::makeLoaderStateScript( $context, $states );
1228 $stateScript = self::filter(
'minify-js', $stateScript );
1231 $out = self::ensureNewline( $out ) . $stateScript;
1233 } elseif ( $states ) {
1234 $this->errors[] =
'Problematic modules: '
1247 public static function ensureNewline( $str ) {
1248 $end = substr( $str, -1 );
1249 if ( $end ===
false || $end ===
'' || $end ===
"\n" ) {
1261 public function getModulesByMessage( $messageKey ) {
1263 foreach ( $this->getModuleNames() as $moduleName ) {
1264 $module = $this->getModule( $moduleName );
1265 if ( in_array( $messageKey, $module->getMessages() ) ) {
1266 $moduleNames[] = $moduleName;
1269 return $moduleNames;
1289 private static function makeLoaderImplementScript(
1290 Context $context, $name, $scripts, $styles, $messages, $templates
1293 if ( $scripts->value ===
'' ) {
1296 $scripts =
new XmlJsCode(
"function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1298 } elseif ( is_array( $scripts ) && isset( $scripts[
'files'] ) ) {
1299 $files = $scripts[
'files'];
1303 if (
$file[
'type'] ===
'script' ) {
1310 $file =
new XmlJsCode(
"function ( require, module, exports ) {\n$content}" );
1316 'main' => $scripts[
'main'],
1317 'files' => XmlJsCode::encodeObject( $files,
true )
1319 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1320 throw new InvalidArgumentException(
'Script must be a string or an array of URLs' );
1333 self::trimArray( $module );
1346 public static function makeMessageSetScript( $messages ) {
1347 return 'mw.messages.set('
1348 . self::encodeJsonForScript( (
object)$messages )
1359 public static function makeCombinedStyles( array $stylePairs ) {
1361 foreach ( $stylePairs as $media => $styles ) {
1364 $styles = (array)$styles;
1365 foreach ( $styles as $style ) {
1366 $style = trim( $style );
1368 if ( $style ===
'' ) {
1375 if ( $media ===
'' || $media ==
'all' ) {
1377 } elseif ( is_string( $media ) ) {
1378 $out[] =
"@media $media {\n" . str_replace(
"\n",
"\n\t",
"\t" . $style ) .
"}";
1395 public static function encodeJsonForScript( $data ) {
1405 $jsonFlags = JSON_UNESCAPED_SLASHES |
1406 JSON_UNESCAPED_UNICODE |
1409 if ( self::inDebugMode() ) {
1410 $jsonFlags |= JSON_PRETTY_PRINT;
1412 return json_encode( $data, $jsonFlags );
1427 public static function makeLoaderStateScript(
1428 Context $context, array $states
1430 return 'mw.loader.state('
1431 . $context->encodeJson( $states )
1435 private static function isEmptyObject( stdClass $obj ) {
1436 foreach ( $obj as $key => $value ) {
1455 private static function trimArray( array &$array ): void {
1456 $i = count( $array );
1458 if ( $array[$i] ===
null
1459 || $array[$i] === []
1460 || ( $array[$i] instanceof
XmlJsCode && $array[$i]->value ===
'{}' )
1461 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1463 unset( $array[$i] );
1495 public static function makeLoaderRegisterScript(
1502 foreach (
$modules as $i => &$module ) {
1504 $index[$module[0]] = $i;
1507 if ( isset( $module[2] ) ) {
1508 foreach ( $module[2] as &$dependency ) {
1509 if ( isset( $index[$dependency] ) ) {
1511 $dependency = $index[$dependency];
1517 array_walk(
$modules, [ self::class,
'trimArray' ] );
1519 return 'mw.loader.register('
1537 public static function makeLoaderSourcesScript(
1538 Context $context, array $sources
1540 return 'mw.loader.addSource('
1541 . $context->encodeJson( $sources )
1551 public static function makeLoaderConditionalScript( $script ) {
1553 return '(RLQ=window.RLQ||[]).push(function(){' .
1554 trim( $script ) .
'});';
1565 public static function makeInlineCodeWithModule(
$modules, $script ) {
1567 return '(RLQ=window.RLQ||[]).push(['
1568 . self::encodeJsonForScript(
$modules ) .
','
1569 .
'function(){' . trim( $script ) .
'}'
1584 public static function makeInlineScript( $script, $nonce =
null ) {
1585 $js = self::makeLoaderConditionalScript( $script );
1587 if ( $nonce ===
null ) {
1588 wfWarn( __METHOD__ .
" did not get nonce. Will break CSP" );
1589 } elseif ( $nonce !==
false ) {
1593 $escNonce =
' nonce="' . htmlspecialchars( $nonce ) .
'"';
1596 return new WrappedString(
1597 Html::inlineScript( $js, $nonce ),
1598 "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1611 public static function makeConfigSetScript( array $configuration ) {
1612 $json = self::encodeJsonForScript( $configuration );
1613 if ( $json ===
false ) {
1615 'JSON serialization of config data failed. ' .
1616 'This usually means the config data is not valid UTF-8.'
1619 return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) .
');';
1621 return "mw.config.set($json);";
1637 public static function makePackedModulesString( array
$modules ) {
1640 $pos = strrpos( $module,
'.' );
1641 $prefix = $pos ===
false ?
'' : substr( $module, 0, $pos );
1642 $suffix = $pos ===
false ? $module : substr( $module, $pos + 1 );
1643 $moduleMap[$prefix][] = $suffix;
1647 foreach ( $moduleMap as $prefix => $suffixes ) {
1648 $p = $prefix ===
'' ?
'' : $prefix .
'.';
1649 $arr[] = $p . implode(
',', $suffixes );
1651 return implode(
'|', $arr );
1665 public static function expandModuleNames(
$modules ) {
1667 $exploded = explode(
'|',
$modules );
1668 foreach ( $exploded as $group ) {
1669 if ( strpos( $group,
',' ) ===
false ) {
1676 $pos = strrpos( $group,
'.' );
1677 if ( $pos ===
false ) {
1679 $retval = array_merge( $retval, explode(
',', $group ) );
1683 $prefix = substr( $group, 0, $pos );
1684 $suffixes = explode(
',', substr( $group, $pos + 1 ) );
1685 foreach ( $suffixes as $suffix ) {
1686 $retval[] =
"$prefix.$suffix";
1702 public static function inDebugMode() {
1703 if ( self::$debugMode ===
null ) {
1705 $resourceLoaderDebug = MediaWikiServices::getInstance()->getMainConfig()->get(
1706 MainConfigNames::ResourceLoaderDebug );
1708 $wgRequest->getCookie(
'resourceLoaderDebug',
'', $resourceLoaderDebug ?
'true' :
'' )
1710 self::$debugMode = Context::debugFromString( $str );
1712 return self::$debugMode;
1725 public static function clearCache() {
1726 self::$debugMode =
null;
1738 public function createLoaderURL(
$source, Context $context,
1739 array $extraQuery = []
1741 $query = self::createLoaderQuery( $context, $extraQuery );
1742 $script = $this->getLoadScript(
$source );
1756 protected static function createLoaderQuery(
1757 Context $context, array $extraQuery = []
1759 return self::makeLoaderQuery(
1760 $context->getModules(),
1761 $context->getLanguage(),
1762 $context->getSkin(),
1763 $context->getUser(),
1764 $context->getVersion(),
1765 $context->getDebug(),
1766 $context->getOnly(),
1767 $context->getRequest()->getBool(
'printable' ),
1789 public static function makeLoaderQuery( array
$modules,
$lang, $skin, $user =
null,
1790 $version =
null,
$debug = Context::DEBUG_OFF, $only =
null,
1791 $printable =
false, $handheld =
null, array $extraQuery = []
1794 'modules' => self::makePackedModulesString(
$modules ),
1800 if (
$lang !== Context::DEFAULT_LANG ) {
1801 $query[
'lang'] =
$lang;
1803 if ( $skin !== Context::DEFAULT_SKIN ) {
1804 $query[
'skin'] = $skin;
1806 if (
$debug !== Context::DEBUG_OFF ) {
1807 $query[
'debug'] = strval(
$debug );
1809 if ( $user !==
null ) {
1810 $query[
'user'] = $user;
1812 if ( $version !==
null ) {
1813 $query[
'version'] = $version;
1815 if ( $only !==
null ) {
1816 $query[
'only'] = $only;
1819 $query[
'printable'] = 1;
1821 $query += $extraQuery;
1837 public static function isValidModuleName( $moduleName ) {
1838 $len = strlen( $moduleName );
1839 return $len <= 255 && strcspn( $moduleName,
'!,|', 0, $len ) === $len;
1853 public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1858 if ( !class_exists( Less_Parser::class ) ) {
1859 throw new MWException(
'MediaWiki requires the less.php parser' );
1862 $importDirs[] =
"$IP/resources/src/mediawiki.less";
1864 $parser =
new Less_Parser;
1865 $parser->ModifyVars( $vars );
1867 $parser->SetImportDirs( array_fill_keys( $importDirs,
'' ) );
1868 $parser->SetOption(
'relativeUrls',
false );
1886 public function expandUrl(
string $base,
string $url ): string {
1888 $isProtoRelative = strpos(
$base,
'//' ) === 0;
1889 if ( $isProtoRelative ) {
1890 $base =
"https:$base";
1893 $baseUrl =
new Net_URL2(
$base );
1894 $ret = $baseUrl->resolve( $url );
1895 if ( $isProtoRelative ) {
1896 $ret->setScheme(
false );
1898 return $ret->getURL();
1918 public static function filter( $filter, $data, array $options = [] ) {
1919 if ( strpos( $data, self::FILTER_NOMIN ) !==
false ) {
1923 if ( isset( $options[
'cache'] ) && $options[
'cache'] ===
false ) {
1924 return self::applyFilter( $filter, $data ) ?? $data;
1927 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1930 $key =
$cache->makeGlobalKey(
1931 'resourceloader-filter',
1933 self::CACHE_VERSION,
1937 $incKey =
"resourceloader_cache.$filter.hit";
1938 $result =
$cache->getWithSetCallback(
1941 function () use ( $filter, $data, &$incKey ) {
1942 $incKey =
"resourceloader_cache.$filter.miss";
1943 return self::applyFilter( $filter, $data );
1946 $stats->increment( $incKey );
1947 if ( $result ===
null ) {
1960 private static function applyFilter( $filter, $data ) {
1961 $data = trim( $data );
1964 $data = ( $filter ===
'minify-css' )
1965 ? CSSMin::minify( $data )
1966 : JavaScriptMinifier::minify( $data );
1967 }
catch ( TimeoutException $e ) {
1969 }
catch ( Exception $e ) {
1988 public static function getUserDefaults(
1990 HookContainer $hookContainer,
1991 UserOptionsLookup $userOptionsLookup
1993 $defaultOptions = $userOptionsLookup->getDefaultOptions();
1994 $keysToExclude = [];
1995 $hookRunner =
new HookRunner( $hookContainer );
1996 $hookRunner->onResourceLoaderExcludeUserOptions( $keysToExclude, $context );
1997 foreach ( $keysToExclude as $excludedKey ) {
1998 unset( $defaultOptions[ $excludedKey ] );
2000 return $defaultOptions;
2011 public static function getSiteConfigSettings(
2012 Context $context,
Config $conf
2017 $contLang = MediaWikiServices::getInstance()->getContentLanguage();
2018 $namespaceIds = $contLang->getNamespaceIds();
2019 $caseSensitiveNamespaces = [];
2020 $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
2021 foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
2022 $namespaceIds[$contLang->lc( $name )] = $index;
2023 if ( !$nsInfo->isCapitalized( $index ) ) {
2024 $caseSensitiveNamespaces[] = $index;
2028 $illegalFileChars = $conf->
get( MainConfigNames::IllegalFileChars );
2031 $skin = $context->getSkin();
2035 'debug' => $context->getDebug(),
2037 'stylepath' => $conf->
get( MainConfigNames::StylePath ),
2038 'wgArticlePath' => $conf->
get( MainConfigNames::ArticlePath ),
2039 'wgScriptPath' => $conf->
get( MainConfigNames::ScriptPath ),
2040 'wgScript' => $conf->
get( MainConfigNames::Script ),
2041 'wgSearchType' => $conf->
get( MainConfigNames::SearchType ),
2042 'wgVariantArticlePath' => $conf->
get( MainConfigNames::VariantArticlePath ),
2043 'wgServer' => $conf->
get( MainConfigNames::Server ),
2044 'wgServerName' => $conf->
get( MainConfigNames::ServerName ),
2045 'wgUserLanguage' => $context->getLanguage(),
2046 'wgContentLanguage' => $contLang->getCode(),
2048 'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
2049 'wgNamespaceIds' => $namespaceIds,
2050 'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
2051 'wgSiteName' => $conf->
get( MainConfigNames::Sitename ),
2052 'wgDBname' => $conf->
get( MainConfigNames::DBname ),
2054 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
2056 'wgExtensionAssetsPath' => $conf->
get( MainConfigNames::ExtensionAssetsPath ),
2067 'wgActionPaths' => (object)$conf->
get( MainConfigNames::ActionPaths ),
2069 'wgTranslateNumerals' => $conf->
get( MainConfigNames::TranslateNumerals ),
2071 'wgExtraSignatureNamespaces' => $conf->
get( MainConfigNames::ExtraSignatureNamespaces ),
2076 Hooks::runner()->onResourceLoaderGetConfigVars( $vars, $skin, $conf );