32 use InvalidArgumentException;
50 use Psr\Log\LoggerAwareInterface;
51 use Psr\Log\LoggerInterface;
52 use Psr\Log\NullLogger;
57 use UnexpectedValueException;
61 use Wikimedia\Minify\CSSMin;
62 use Wikimedia\Minify\JavaScriptMinifier;
64 use Wikimedia\RequestTimeout\TimeoutException;
65 use Wikimedia\ScopedCallback;
66 use Wikimedia\Timestamp\ConvertibleTimestamp;
67 use Wikimedia\WrappedString;
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;
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 ) {
524 $updatesByEntity = $this->depStoreUpdateBuffer;
525 $this->depStoreUpdateBuffer = [];
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];
645 $hash = hash(
'fnv132', $value );
649 \Wikimedia\base_convert( $hash, 16, 36, self::HASH_LENGTH ),
666 $this->logger->warning(
668 $context + [
'exception' => $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 ) {
694 'Calculating version for "{module}" failed: {exception}',
764 if ( $module->getGroup() === Module::GROUP_PRIVATE ) {
766 $this->logger->debug(
"Request for private module '$name' denied" );
767 $this->errors[] =
"Cannot build private module \"$name\"";
779 }
catch ( TimeoutException $e ) {
781 }
catch ( Exception $e ) {
790 $etag =
'W/"' . $versionHash .
'"';
798 if ( $this->useFileCache ) {
813 $warnings = ob_get_contents();
814 if ( strlen( $warnings ) ) {
815 $this->errors[] = $warnings;
820 if ( $fileCache && !$this->errors && $missing === [] &&
822 if ( $fileCache->isCacheWorthy() ) {
824 $fileCache->saveText( $response );
826 $fileCache->incrMissesRecent( $context->
getRequest() );
837 $response = implode(
"\n\n", $this->errors );
838 } elseif ( $this->errors ) {
839 $errorText = implode(
"\n\n", $this->errors );
842 $errorResponse .=
'if (window.console && console.error) { console.error('
848 $response = $errorResponse . $response;
860 $statStart = $_SERVER[
'REQUEST_TIME_FLOAT'];
861 return new ScopedCallback(
static function () use ( $statStart ) {
862 $statTiming = microtime(
true ) - $statStart;
864 $stats->timing(
'resourceloader.responseTime', $statTiming * 1000 );
884 $maxage = self::MAXAGE_RECOVER;
893 $this->logger->info(
'Client and server registry version out of sync' );
894 $maxage = self::MAXAGE_RECOVER;
895 } elseif ( $context->
getVersion() ===
null ) {
899 $maxage = $this->maxageUnversioned;
903 $maxage = $this->maxageVersioned;
908 header(
'Content-Type: text/plain; charset=utf-8' );
910 $context->
getImageObj()->sendResponseHeaders( $context );
912 } elseif ( $context->
getOnly() ===
'styles' ) {
913 header(
'Content-Type: text/css; charset=utf-8' );
914 header(
'Access-Control-Allow-Origin: *' );
916 header(
'Content-Type: text/javascript; charset=utf-8' );
920 header(
'ETag: ' . $etag );
923 header(
'Cache-Control: private, no-cache, must-revalidate' );
924 header(
'Pragma: no-cache' );
928 $staleDirective = ( $maxage > self::MAXAGE_RECOVER
929 ?
", stale-while-revalidate=" . min( 60, intval( $maxage / 2 ) )
932 header(
"Cache-Control: public, max-age=$maxage, s-maxage=$maxage" . $staleDirective );
933 header(
'Expires: ' . ConvertibleTimestamp::convert( TS_RFC2822, time() + $maxage ) );
935 foreach ( $extra as
$header ) {
955 if ( $clientKeys !==
false && !$context->
getDebug() && in_array( $etag, $clientKeys ) ) {
969 $this->sendResponseHeaders( $context, $etag,
false );
992 ? $this->maxageUnversioned
993 : $this->maxageVersioned;
995 $minTime = time() - $maxage;
996 $good = $fileCache->
isCacheGood( ConvertibleTimestamp::convert( TS_MW, $minTime ) );
1007 $this->sendResponseHeaders( $context, $etag,
false );
1012 $warnings = ob_get_contents();
1013 if ( strlen( $warnings ) ) {
1014 $response = self::makeComment( $warnings ) . $response;
1019 echo $response .
"\n/* Cached {$ts} */";
1037 $encText = str_replace(
'*/',
'* /', $text );
1038 return "/*\n$encText\n*/\n";
1048 return self::makeComment( self::formatExceptionNoComment( $e ) );
1080 array
$modules, array $missing = []
1082 if (
$modules === [] && $missing === [] ) {
1092 $data = $image->getImageData( $context );
1093 if ( $data ===
false ) {
1095 $this->errors[] =
'Image generation failed';
1101 foreach ( $missing as $name ) {
1102 $states[$name] =
'missing';
1106 $filter = $only ===
'styles' ?
'minify-css' :
'minify-js';
1107 $debug = (bool)$context->
getDebug();
1110 foreach (
$modules as $name => $module ) {
1112 $content = $module->getModuleContent( $context );
1113 $implementKey = $name .
'@' . $module->getVersionHash( $context );
1116 if ( isset(
$content[
'headers'] ) ) {
1117 $this->extraHeaders = array_merge( $this->extraHeaders,
$content[
'headers'] );
1124 if ( is_string( $scripts ) ) {
1126 $strContent = $scripts;
1127 } elseif ( is_array( $scripts ) ) {
1129 $strContent = self::makeLoaderImplementScript(
1143 $strContent = isset( $styles[
'css'] ) ? implode(
'', $styles[
'css'] ) :
'';
1146 $scripts =
$content[
'scripts'] ??
'';
1147 if ( is_string( $scripts ) ) {
1148 if ( $name ===
'site' || $name ===
'user' ) {
1154 $scripts = self::filter(
'minify-js', $scripts );
1160 $strContent = self::makeLoaderImplementScript(
1173 $strContent = self::ensureNewline( $strContent );
1175 $strContent = self::filter( $filter, $strContent, [
1179 'cache' => !$module->shouldEmbedModule( $context )
1183 if ( $only ===
'scripts' ) {
1185 $out .= self::ensureNewline( $strContent );
1187 $out .= $strContent;
1189 }
catch ( TimeoutException $e ) {
1191 }
catch ( Exception $e ) {
1192 $this->outputErrorAndLog( $e,
'Generating module package failed: {exception}' );
1195 $states[$name] =
'error';
1202 if (
$modules && $only ===
'scripts' ) {
1205 foreach (
$modules as $name => $module ) {
1206 $states[$name] =
'ready';
1212 $stateScript = self::makeLoaderStateScript( $context, $states );
1214 $stateScript = self::filter(
'minify-js', $stateScript );
1217 $out = self::ensureNewline( $out ) . $stateScript;
1219 } elseif ( $states ) {
1220 $this->errors[] =
'Problematic modules: '
1236 public static function ensureNewline( $str ) {
1237 $end = substr( $str, -1 );
1238 if ( $end ===
false || $end ===
'' || $end ===
"\n" ) {
1250 public function getModulesByMessage( $messageKey ) {
1252 foreach ( $this->getModuleNames() as $moduleName ) {
1253 $module = $this->getModule( $moduleName );
1254 if ( in_array( $messageKey, $module->getMessages() ) ) {
1255 $moduleNames[] = $moduleName;
1258 return $moduleNames;
1280 private static function makeLoaderImplementScript(
1281 $name, $scripts, $styles, $messages, $templates
1284 if ( $scripts->value ===
'' ) {
1287 $scripts =
new XmlJsCode(
"function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" );
1289 } elseif ( is_array( $scripts ) && isset( $scripts[
'files'] ) ) {
1290 $files = $scripts[
'files'];
1291 foreach ( $files as &
$file ) {
1294 if (
$file[
'type'] ===
'script' ) {
1301 $file =
new XmlJsCode(
"function ( require, module, exports ) {\n$content}" );
1307 'main' => $scripts[
'main'],
1310 } elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) {
1311 throw new InvalidArgumentException(
'Script must be a string or an array of URLs' );
1321 $messages ?? (
object)[],
1324 self::trimArray( $module );
1338 public static function makeCombinedStyles( array $stylePairs ) {
1340 foreach ( $stylePairs as $media => $styles ) {
1343 $styles = (array)$styles;
1344 foreach ( $styles as $style ) {
1345 $style = trim( $style );
1347 if ( $style ===
'' ) {
1354 if ( $media ===
'' || $media ==
'all' ) {
1356 } elseif ( is_string( $media ) ) {
1357 $out[] =
"@media $media {\n" . str_replace(
"\n",
"\n\t",
"\t" . $style ) .
"}";
1372 private static function encodeJsonForScript( $data ) {
1382 $jsonFlags = JSON_UNESCAPED_SLASHES |
1383 JSON_UNESCAPED_UNICODE |
1386 if ( self::inDebugMode() ) {
1387 $jsonFlags |= JSON_PRETTY_PRINT;
1389 return json_encode( $data, $jsonFlags );
1400 public static function makeLoaderStateScript(
1401 Context $context, array $states
1403 return 'mw.loader.state('
1407 . @$context->encodeJson( $states )
1411 private static function isEmptyObject( stdClass $obj ) {
1412 foreach ( $obj as $value ) {
1431 private static function trimArray( array &$array ): void {
1432 $i = count( $array );
1434 if ( $array[$i] ===
null
1435 || $array[$i] === []
1436 || ( $array[$i] instanceof
XmlJsCode && $array[$i]->value ===
'{}' )
1437 || ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) )
1439 unset( $array[$i] );
1471 public static function makeLoaderRegisterScript(
1478 foreach (
$modules as $i => &$module ) {
1480 $index[$module[0]] = $i;
1483 if ( isset( $module[2] ) ) {
1484 foreach ( $module[2] as &$dependency ) {
1485 if ( isset( $index[$dependency] ) ) {
1487 $dependency = $index[$dependency];
1493 array_walk(
$modules, [ self::class,
'trimArray' ] );
1495 return 'mw.loader.register('
1513 public static function makeLoaderSourcesScript(
1514 Context $context, array $sources
1516 return 'mw.loader.addSource('
1517 . $context->encodeJson( $sources )
1527 public static function makeLoaderConditionalScript( $script ) {
1529 return '(RLQ=window.RLQ||[]).push(function(){' .
1530 trim( $script ) .
'});';
1541 public static function makeInlineCodeWithModule(
$modules, $script ) {
1543 return '(RLQ=window.RLQ||[]).push(['
1544 . self::encodeJsonForScript(
$modules ) .
','
1545 .
'function(){' . trim( $script ) .
'}'
1560 public static function makeInlineScript( $script, $nonce =
null ) {
1561 $js = self::makeLoaderConditionalScript( $script );
1563 if ( $nonce ===
null ) {
1564 wfWarn( __METHOD__ .
" did not get nonce. Will break CSP" );
1565 } elseif ( $nonce !==
false ) {
1569 $escNonce =
' nonce="' . htmlspecialchars( $nonce ) .
'"';
1572 return new WrappedString(
1573 Html::inlineScript( $js, $nonce ),
1574 "<script$escNonce>(RLQ=window.RLQ||[]).push(function(){",
1587 public static function makeConfigSetScript( array $configuration ) {
1588 $json = self::encodeJsonForScript( $configuration );
1589 if ( $json ===
false ) {
1591 'JSON serialization of config data failed. ' .
1592 'This usually means the config data is not valid UTF-8.'
1595 return 'mw.log.error(' . self::encodeJsonForScript( $e->__toString() ) .
');';
1597 return "mw.config.set($json);";
1613 public static function makePackedModulesString( array
$modules ) {
1616 $pos = strrpos( $module,
'.' );
1617 $prefix = $pos ===
false ?
'' : substr( $module, 0, $pos );
1618 $suffix = $pos ===
false ? $module : substr( $module, $pos + 1 );
1619 $moduleMap[$prefix][] = $suffix;
1623 foreach ( $moduleMap as $prefix => $suffixes ) {
1624 $p = $prefix ===
'' ?
'' : $prefix .
'.';
1625 $arr[] = $p . implode(
',', $suffixes );
1627 return implode(
'|', $arr );
1641 public static function expandModuleNames(
$modules ) {
1643 $exploded = explode(
'|',
$modules );
1644 foreach ( $exploded as $group ) {
1645 if ( strpos( $group,
',' ) ===
false ) {
1652 $pos = strrpos( $group,
'.' );
1653 if ( $pos ===
false ) {
1655 $retval = array_merge( $retval, explode(
',', $group ) );
1659 $prefix = substr( $group, 0, $pos );
1660 $suffixes = explode(
',', substr( $group, $pos + 1 ) );
1661 foreach ( $suffixes as $suffix ) {
1662 $retval[] =
"$prefix.$suffix";
1678 public static function inDebugMode() {
1679 if ( self::$debugMode ===
null ) {
1681 $resourceLoaderDebug = MediaWikiServices::getInstance()->getMainConfig()->get(
1682 MainConfigNames::ResourceLoaderDebug );
1684 $wgRequest->getCookie(
'resourceLoaderDebug',
'', $resourceLoaderDebug ?
'true' :
'' )
1686 self::$debugMode = Context::debugFromString( $str );
1688 return self::$debugMode;
1701 public static function clearCache() {
1702 self::$debugMode =
null;
1714 public function createLoaderURL(
$source, Context $context,
1715 array $extraQuery = []
1717 $query = self::createLoaderQuery( $context, $extraQuery );
1718 $script = $this->getLoadScript(
$source );
1732 protected static function createLoaderQuery(
1733 Context $context, array $extraQuery = []
1735 return self::makeLoaderQuery(
1736 $context->getModules(),
1737 $context->getLanguage(),
1738 $context->getSkin(),
1739 $context->getUser(),
1740 $context->getVersion(),
1741 $context->getDebug(),
1742 $context->getOnly(),
1743 $context->getRequest()->getBool(
'printable' ),
1765 public static function makeLoaderQuery( array
$modules,
$lang, $skin, $user =
null,
1766 $version =
null, $debug = Context::DEBUG_OFF, $only =
null,
1767 $printable =
false, $handheld =
null, array $extraQuery = []
1770 'modules' => self::makePackedModulesString(
$modules ),
1776 if (
$lang !== Context::DEFAULT_LANG ) {
1777 $query[
'lang'] =
$lang;
1779 if ( $skin !== Context::DEFAULT_SKIN ) {
1780 $query[
'skin'] = $skin;
1782 if ( $debug !== Context::DEBUG_OFF ) {
1783 $query[
'debug'] = strval( $debug );
1785 if ( $user !==
null ) {
1786 $query[
'user'] = $user;
1788 if ( $version !==
null ) {
1789 $query[
'version'] = $version;
1791 if ( $only !==
null ) {
1792 $query[
'only'] = $only;
1795 $query[
'printable'] = 1;
1797 $query += $extraQuery;
1813 public static function isValidModuleName( $moduleName ) {
1814 $len = strlen( $moduleName );
1815 return $len <= 255 && strcspn( $moduleName,
'!,|', 0, $len ) === $len;
1828 public function getLessCompiler( array $vars = [], array $importDirs = [] ) {
1833 if ( !class_exists( Less_Parser::class ) ) {
1834 throw new RuntimeException(
'MediaWiki requires the less.php parser' );
1837 $importDirs[] =
"$IP/resources/src/mediawiki.less";
1839 $parser =
new Less_Parser;
1840 $parser->ModifyVars( $vars );
1841 $parser->SetOption(
'relativeUrls',
false );
1844 $formattedImportDirs = array_fill_keys( $importDirs,
'' );
1846 $formattedImportDirs[] =
static function (
$path ) {
1849 '@wikimedia/codex-icons/' =>
"$IP/resources/lib/codex-icons/",
1850 'mediawiki.skin.codex-design-tokens/' =>
"$IP/resources/lib/codex-design-tokens/",
1851 '@wikimedia/codex-design-tokens/' =>
static function ( $unused_path ) {
1852 throw new RuntimeException(
1853 'Importing from @wikimedia/codex-design-tokens is not supported. ' .
1854 "To use the Codex tokens, use `@import 'mediawiki.skin.variables.less';` instead."
1858 foreach ( $importMap as $importPath => $substPath ) {
1859 if ( str_starts_with(
$path, $importPath ) ) {
1860 $restOfPath = substr(
$path, strlen( $importPath ) );
1861 if ( is_callable( $substPath ) ) {
1862 $resolvedPath = call_user_func( $substPath, $restOfPath );
1864 $filePath = $substPath . $restOfPath;
1866 $resolvedPath =
null;
1867 if ( file_exists( $filePath ) ) {
1868 $resolvedPath = $filePath;
1869 } elseif ( file_exists(
"$filePath.less" ) ) {
1870 $resolvedPath =
"$filePath.less";
1874 if ( $resolvedPath !==
null ) {
1876 Less_Environment::normalizePath( $resolvedPath ),
1877 Less_Environment::normalizePath( dirname(
$path ) )
1884 return [
null, null ];
1886 $parser->SetImportDirs( $formattedImportDirs );
1904 public function expandUrl(
string $base,
string $url ): string {
1906 $isProtoRelative = strpos( $base,
'//' ) === 0;
1907 if ( $isProtoRelative ) {
1908 $base =
"https:$base";
1911 $baseUrl =
new Net_URL2( $base );
1912 $ret = $baseUrl->resolve( $url );
1913 if ( $isProtoRelative ) {
1914 $ret->setScheme(
false );
1916 return $ret->getURL();
1936 public static function filter( $filter, $data, array $options = [] ) {
1937 if ( strpos( $data, self::FILTER_NOMIN ) !==
false ) {
1941 if ( isset( $options[
'cache'] ) && $options[
'cache'] ===
false ) {
1942 return self::applyFilter( $filter, $data ) ?? $data;
1945 $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
1948 $key = $cache->makeGlobalKey(
1949 'resourceloader-filter',
1951 self::CACHE_VERSION,
1955 $incKey =
"resourceloader_cache.$filter.hit";
1956 $result = $cache->getWithSetCallback(
1959 function () use ( $filter, $data, &$incKey ) {
1960 $incKey =
"resourceloader_cache.$filter.miss";
1961 return self::applyFilter( $filter, $data );
1964 $stats->increment( $incKey );
1967 return $result ?? $data;
1975 private static function applyFilter( $filter, $data ) {
1976 $data = trim( $data );
1979 $data = ( $filter ===
'minify-css' )
1980 ? CSSMin::minify( $data )
1981 : JavaScriptMinifier::minify( $data );
1982 }
catch ( TimeoutException $e ) {
1984 }
catch ( Exception $e ) {
2003 public static function getUserDefaults(
2005 HookContainer $hookContainer,
2006 UserOptionsLookup $userOptionsLookup
2008 $defaultOptions = $userOptionsLookup->getDefaultOptions();
2009 $keysToExclude = [];
2010 $hookRunner =
new HookRunner( $hookContainer );
2011 $hookRunner->onResourceLoaderExcludeUserOptions( $keysToExclude, $context );
2012 foreach ( $keysToExclude as $excludedKey ) {
2013 unset( $defaultOptions[ $excludedKey ] );
2015 return $defaultOptions;
2026 public static function getSiteConfigSettings(
2027 Context $context,
Config $conf
2029 $services = MediaWikiServices::getInstance();
2033 $contLang = $services->getContentLanguage();
2034 $namespaceIds = $contLang->getNamespaceIds();
2035 $caseSensitiveNamespaces = [];
2036 $nsInfo = $services->getNamespaceInfo();
2037 foreach ( $nsInfo->getCanonicalNamespaces() as $index => $name ) {
2038 $namespaceIds[$contLang->lc( $name )] = $index;
2039 if ( !$nsInfo->isCapitalized( $index ) ) {
2040 $caseSensitiveNamespaces[] = $index;
2044 $illegalFileChars = $conf->
get( MainConfigNames::IllegalFileChars );
2047 $skin = $context->getSkin();
2051 'debug' => $context->getDebug(),
2053 'stylepath' => $conf->
get( MainConfigNames::StylePath ),
2054 'wgArticlePath' => $conf->
get( MainConfigNames::ArticlePath ),
2055 'wgScriptPath' => $conf->
get( MainConfigNames::ScriptPath ),
2056 'wgScript' => $conf->
get( MainConfigNames::Script ),
2057 'wgSearchType' => $conf->
get( MainConfigNames::SearchType ),
2058 'wgVariantArticlePath' => $conf->
get( MainConfigNames::VariantArticlePath ),
2059 'wgServer' => $conf->
get( MainConfigNames::Server ),
2060 'wgServerName' => $conf->
get( MainConfigNames::ServerName ),
2061 'wgUserLanguage' => $context->getLanguage(),
2062 'wgContentLanguage' => $contLang->getCode(),
2064 'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
2065 'wgNamespaceIds' => $namespaceIds,
2066 'wgContentNamespaces' => $nsInfo->getContentNamespaces(),
2067 'wgSiteName' => $conf->
get( MainConfigNames::Sitename ),
2068 'wgDBname' => $conf->
get( MainConfigNames::DBname ),
2069 'wgWikiID' => WikiMap::getCurrentWikiId(),
2070 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
2071 'wgCommentCodePointLimit' => CommentStore::COMMENT_CHARACTER_LIMIT,
2072 'wgExtensionAssetsPath' => $conf->
get( MainConfigNames::ExtensionAssetsPath ),
2083 'wgActionPaths' => (object)$conf->
get( MainConfigNames::ActionPaths ),
2085 'wgTranslateNumerals' => $conf->
get( MainConfigNames::TranslateNumerals ),
2087 'wgExtraSignatureNamespaces' => $conf->
get( MainConfigNames::ExtraSignatureNamespaces ),
2088 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
2089 'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
2092 (
new HookRunner( $services->getHookContainer() ) )
2093 ->onResourceLoaderGetConfigVars( $vars, $skin, $conf );
2102 public function getErrors() {
2103 return $this->errors;
2107 class_alias( ResourceLoader::class,
'ResourceLoader' );
const MW_VERSION
The running version of MediaWiki.
wfWarn( $msg, $callerOffset=1, $level=E_USER_NOTICE)
Send a warning either to the debug log or in a PHP error depending on $wgDevelopmentWarnings.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfUrlProtocols( $includeProtocolRelative=true)
Returns a regular expression of url protocols.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfResetOutputBuffers( $resetGzipEncoding=true)
Clear away any user-level output buffers, discarding contents.
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
Class representing a cache/ephemeral data store.
Class for managing the deferral of updates within the scope of a PHP script invocation.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
Load JSON files, and uses a Processor to extract information.
isCacheGood( $timestamp='')
Check if up to date cache file exists.
fetchText()
Get the uncompressed text from the cache.
cacheTimestamp()
Get the last-modified timestamp of the cache file.
Simple store for keeping values in an associative array for the current process.
static header( $code)
Output an HTTP status code header.
Handler class for MWExceptions.
static getRedactedTraceAsString(Throwable $e)
Generate a string representation of a throwable's stack trace.
static logException(Throwable $e, $catcher=self::CAUGHT_BY_OTHER, $extraData=[])
Log a throwable to the exception log (if enabled).
static getPublicLogMessage(Throwable $e)
static getLogMessage(Throwable $e)
Get a message formatting the throwable message and its origin.
Class to expose exceptions to the client (API bots, users, admins using CLI scripts)
static shouldShowExceptionDetails()
A class containing constants representing the names of configuration variables.
Class for tracking request-level classification information for profiling/stats/logging.
Context object that contains information about the state of a specific ResourceLoader web request.
encodeJson( $data)
Wrapper around json_encode that avoids needless escapes, and pretty-prints in debug mode.
getImageObj()
If this is a request for an image, get the Image object.
Functions to get cache objects.
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from configuration)
static getLocalClusterInstance()
Get the main cluster-local cache object.
This is one of the Core classes and should be read at least once by any new developers.
static transformCssMedia( $media)
Transform "media" attribute based on request parameters.
ResourceLoader request result caching in the file system.
static useFileCache(RL\Context $context)
Check if an RL request can be cached.
static newFromContext(RL\Context $context)
Construct an ResourceFileCache from a context.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form stripping il...
const GETHEADER_LIST
Flag to make WebRequest::getHeader return an array of values.
A wrapper class which causes Xml::encodeJsVar() and Xml::encodeJsCall() to interpret a given string a...
static encodeObject( $obj, $pretty=false)
Encode an object containing XmlJsCode objects.
Module of static functions for generating XML.
static encodeJsCall( $name, $args, $pretty=false)
Create a call to a JavaScript function.
Interface for configuration instances.
get( $name)
Get a configuration variable such as "Sitename" or "UploadMaintenance.".
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
if(!isset( $args[0])) $lang
if(count( $args)< 1) $tracker