29 use InvalidArgumentException;
37 use Wikimedia\Minify\CSSMin;
38 use Wikimedia\RequestTimeout\TimeoutException;
103 private $expandedPackageFiles = [];
109 private $fullyExpandedPackageFiles = [];
175 $hasTemplates =
false;
182 foreach ( $options as $member => $option ) {
189 $this->{$member} = is_array( $option ) ? $option : [ $option ];
192 $hasTemplates =
true;
193 $this->{$member} = is_array( $option ) ? $option : [ $option ];
196 case 'languageScripts':
199 if ( !is_array( $option ) ) {
200 throw new InvalidArgumentException(
201 "Invalid collated file path list error. " .
202 "'$option' given, array expected."
205 foreach ( $option as $key => $value ) {
206 if ( !is_string( $key ) ) {
207 throw new InvalidArgumentException(
208 "Invalid collated file path list key error. " .
209 "'$key' given, string expected."
212 $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
216 $this->deprecated = $option;
223 $option = array_values( array_unique( (array)$option ) );
226 $this->{$member} = $option;
231 $this->{$member} = (string)$option;
236 $this->{$member} = (bool)$option;
240 if ( isset( $options[
'scripts'] ) && isset( $options[
'packageFiles'] ) ) {
241 throw new InvalidArgumentException(
"A module may not set both 'scripts' and 'packageFiles'" );
243 if ( isset( $options[
'packageFiles'] ) && isset( $options[
'skinScripts'] ) ) {
244 throw new InvalidArgumentException(
"Options 'skinScripts' and 'packageFiles' cannot be used together." );
246 if ( $hasTemplates ) {
247 $this->dependencies[] =
'mediawiki.template';
249 foreach ( $this->templates as $alias => $templatePath ) {
250 if ( is_int( $alias ) ) {
251 $alias = $this->
getPath( $templatePath );
253 $suffix = explode(
'.', $alias );
254 $suffix = end( $suffix );
255 $compilerModule =
'mediawiki.template.' . $suffix;
256 if ( $suffix !==
'html' && !in_array( $compilerModule, $this->dependencies ) ) {
257 $this->dependencies[] = $compilerModule;
288 if ( isset( $options[
'remoteExtPath'] ) ) {
291 $remoteBasePath = $extensionAssetsPath .
'/' . $options[
'remoteExtPath'];
294 if ( isset( $options[
'remoteSkinPath'] ) ) {
300 if ( array_key_exists(
'localBasePath', $options ) ) {
304 if ( array_key_exists(
'remoteBasePath', $options ) ) {
334 if (
$file[
'type'] ===
'script+style' ) {
335 $file[
'content'] =
$file[
'content'][
'script'];
336 $file[
'type'] =
'script';
339 if ( $deprecationScript ) {
341 $mainFile[
'content'] = $deprecationScript . $mainFile[
'content'];
346 $files = $this->getScriptFiles( $context );
347 return $deprecationScript . $this->readScriptFiles( $context, $files );
360 foreach ( $this->getScriptFiles( $context ) as
$file ) {
361 if ( isset(
$file[
'filePath'] ) ) {
364 $url = $rl->expandUrl( $server, $url );
381 && !$this->packageFiles
383 && !$this->hasGeneratedScripts();
391 private function hasGeneratedScripts() {
393 [ $this->scripts, $this->languageScripts, $this->skinScripts, $this->debugScripts ]
397 if ( is_array( $script ) ) {
398 if ( isset( $script[
'callback'] ) || isset( $script[
'versionCallback'] ) ) {
422 if (
$file[
'type'] ===
'script+style' ) {
424 $file[
'content'][
'style'],
425 $file[
'content'][
'styleLang'],
446 if ( $this->hasGeneratedStyles ) {
449 return parent::getStyleURLsForDebug( $context );
454 foreach ( $this->
getStyleFiles( $context ) as $mediaType => $list ) {
455 $urls[$mediaType] = [];
456 foreach ( $list as
$file ) {
501 private function getFileContents( $localPath,
$type ) {
502 if ( !is_file( $localPath ) ) {
503 throw new RuntimeException(
"$type file not found or not a file: \"$localPath\"" );
505 return $this->
stripBom( file_get_contents( $localPath ) );
512 if ( !$this->skipFunction ) {
515 $localPath = $this->
getLocalPath( $this->skipFunction );
516 return $this->getFileContents( $localPath,
'skip function' );
541 private function getFileHashes(
Context $context ) {
545 foreach ( $filePaths as $filePath ) {
553 $expandedPackageFiles = $this->expandPackageFiles( $context );
554 if ( $expandedPackageFiles ) {
555 foreach ( $expandedPackageFiles[
'files'] as $fileInfo ) {
556 if ( isset( $fileInfo[
'filePath'] ) ) {
558 $filePath = $fileInfo[
'filePath'];
559 $files[] = $filePath->getLocalPath();
565 $scriptFileInfos = $this->getScriptFiles( $context );
566 foreach ( $scriptFileInfos as $fileInfo ) {
567 if ( isset( $fileInfo[
'filePath'] ) ) {
569 $filePath = $fileInfo[
'filePath'];
570 $files[] = $filePath->getLocalPath();
574 foreach ( $this->templates as $filePath ) {
578 if ( $this->skipFunction ) {
590 $files = array_unique( $files );
606 $summary = parent::getDefinitionSummary( $context );
624 $options[$member] = $this->{$member};
628 $packageSummaries = [];
638 foreach (
$packageFiles[
'files'] as $fileName => $fileInfo ) {
639 $packageSummaries[$fileName] =
640 $fileInfo[
'definitionSummary'] ?? $fileInfo[
'content'] ??
null;
644 $scriptFiles = $this->getScriptFiles( $context );
645 $scriptSummaries = [];
646 foreach ( $scriptFiles as $fileName => $fileInfo ) {
647 $scriptSummaries[$fileName] =
648 $fileInfo[
'definitionSummary'] ?? $fileInfo[
'content'] ??
null;
652 'options' => $options,
653 'packageFiles' => $packageSummaries,
654 'scripts' => $scriptSummaries,
655 'fileHashes' => $this->getFileHashes( $context ),
661 $summary[] = [
'lessVars' => $lessVars ];
671 if ( $this->vueComponentParser ===
null ) {
683 return $path->getPath();
695 if (
$path->getLocalBasePath() !==
null ) {
696 return $path->getLocalPath();
701 return "{$this->localBasePath}/$path";
710 if (
$path->getRemoteBasePath() !==
null ) {
711 return $path->getRemotePath();
716 if ( $this->remoteBasePath ===
'/' ) {
719 return "{$this->remoteBasePath}/$path";
731 return preg_match(
'/\.less$/i',
$path ) ?
'less' :
'css';
741 if ( preg_match(
'/\.json$/i',
$path ) ) {
744 if ( preg_match(
'/\.vue$/i',
$path ) ) {
757 private static function collateStyleFilesByMedia( array $list ) {
759 foreach ( $list as $key => $value ) {
760 if ( is_int( $key ) ) {
762 $collatedFiles[
'all'][] = $value;
763 } elseif ( is_array( $value ) ) {
765 $optionValue = $value[
'media'] ??
'all';
766 $collatedFiles[$optionValue][] = $key;
769 return $collatedFiles;
782 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
799 private function getScriptFiles(
Context $context ): array {
803 'scripts' => $this->scripts,
804 'languageScripts' => $this->getLanguageScripts( $context->getLanguage() ),
805 'skinScripts' => self::
tryForKey( $this->skinScripts, $context->getSkin(),
'default' ),
812 foreach ( $filesByCategory as $category => $files ) {
813 foreach ( $files as $key => $fileInfo ) {
814 $expandedFileInfo = $this->expandFileInfo( $context, $fileInfo,
"$category\[$key]" );
815 $expandedFiles[$expandedFileInfo[
'name']] = $expandedFileInfo;
819 return $expandedFiles;
829 private function getLanguageScripts(
string $lang ): array {
830 $scripts = self::tryForKey( $this->languageScripts,
$lang );
837 if ( $this->languageScripts ) {
839 ->getLanguageFallback()
841 foreach ( $fallbacks as
$lang ) {
842 $scripts = self::tryForKey( $this->languageScripts,
$lang );
853 $moduleName = $this->getName();
854 foreach ( $moduleSkinStyles as $skinName => $overrides ) {
857 if ( isset( $this->skinStyles[$skinName] ) ) {
863 if ( isset( $overrides[$moduleName] ) ) {
864 $paths = (array)$overrides[$moduleName];
866 } elseif ( isset( $overrides[
'+' . $moduleName] ) ) {
867 $paths = (array)$overrides[
'+' . $moduleName];
868 $styleFiles = isset( $this->skinStyles[
'default'] ) ?
869 (array)$this->skinStyles[
'default'] :
877 [ $localBasePath, $remoteBasePath ] = self::extractBasePaths( $overrides );
879 foreach ( $paths as
$path ) {
880 $styleFiles[] =
new FilePath(
$path, $localBasePath, $remoteBasePath );
883 $this->skinStyles[$skinName] = $styleFiles;
895 return array_merge_recursive(
896 self::collateStyleFilesByMedia( $this->styles ),
897 self::collateStyleFilesByMedia(
898 self::tryForKey( $this->skinStyles, $context->
getSkin(),
'default' )
911 return self::collateStyleFilesByMedia(
912 self::tryForKey( $this->skinStyles, $skinName )
923 $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
926 $internalSkinNames = array_keys( $skinFactory->getInstalledSkins() );
927 $internalSkinNames[] =
'default';
929 foreach ( $internalSkinNames as $internalSkinName ) {
930 $styleFiles = array_merge_recursive(
932 $this->getSkinStyleFiles( $internalSkinName )
945 $collatedStyleFiles = array_merge_recursive(
946 self::collateStyleFilesByMedia( $this->styles ),
947 $this->getAllSkinStyleFiles()
952 foreach ( $collatedStyleFiles as $styleFiles ) {
953 foreach ( $styleFiles as $styleFile ) {
954 $result[] = $this->getLocalPath( $styleFile );
968 private function readScriptFiles(
Context $context, array $scripts ) {
970 foreach ( $scripts as $fileInfo ) {
971 $this->readFileInfo( $context, $fileInfo );
973 $js .= ResourceLoader::ensureNewline( $fileInfo[
'content'] );
990 foreach ( $styles as $media => $files ) {
991 $uniqueFiles = array_unique( $files, SORT_REGULAR );
993 foreach ( $uniqueFiles as
$file ) {
994 $styleFiles[] = $this->readStyleFile(
$file, $context );
996 $styles[$media] = implode(
"\n", $styleFiles );
1012 $localPath = $this->getLocalPath(
$path );
1013 $style = $this->getFileContents( $localPath,
'style' );
1014 $styleLang = $this->getStyleSheetLang( $localPath );
1016 return $this->processStyle( $style, $styleLang,
$path, $context );
1036 $localPath = $this->getLocalPath(
$path );
1037 $remotePath = $this->getRemotePath(
$path );
1039 if ( $styleLang ===
'less' ) {
1040 $style = $this->compileLessString( $style, $localPath, $context );
1041 $this->hasGeneratedStyles =
true;
1044 if ( $this->getFlip( $context ) ) {
1045 $style = CSSJanus::transform(
1052 $localDir = dirname( $localPath );
1053 $remoteDir = dirname( $remotePath );
1055 $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
1056 foreach ( $localFileRefs as
$file ) {
1057 if ( is_file(
$file ) ) {
1058 $this->localFileRefs[] =
$file;
1060 $this->missingLocalFileRefs[] =
$file;
1065 return CSSMin::remap( $style, $localDir, $remoteDir,
true );
1074 return $context->
getDirection() ===
'rtl' && !$this->noflip;
1083 return $this->targets;
1093 $canBeStylesOnly = !(
1096 || $this->debugScripts
1098 || $this->languageScripts
1099 || $this->skinScripts
1100 || $this->dependencies
1102 || $this->skipFunction
1103 || $this->packageFiles
1105 return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
1126 $skinName = $context->
getSkin();
1129 if ( isset( $skinImportPaths[ $skinName ] ) ) {
1130 $importDirs[] = $skinImportPaths[ $skinName ];
1133 $vars = $this->getLessVars( $context );
1139 'importDirs' => $importDirs,
1141 $key = $cache->makeGlobalKey(
1142 'resourceloader-less',
1144 hash(
'md4', $style ),
1145 hash(
'md4', serialize( $compilerParams ) )
1150 $data = $cache->get( $key );
1155 $compiler = $context->
getResourceLoader()->getLessCompiler( $vars, $importDirs );
1157 $css = $compiler->parse( $style, $stylePath )->getCss();
1161 $files = $compiler->AllParsedFiles();
1164 'files' => Module::getRelativePaths( $files ),
1167 $cache->set( $key, $data, $cache::TTL_DAY );
1170 foreach ( Module::expandRelativePaths( $data[
'files'] ) as
$path ) {
1171 $this->localFileRefs[] =
$path;
1174 return $data[
'css'];
1185 foreach ( $this->templates as $alias => $templatePath ) {
1187 if ( is_int( $alias ) ) {
1188 $alias = $this->getPath( $templatePath );
1190 $localPath = $this->getLocalPath( $templatePath );
1191 $content = $this->getFileContents( $localPath,
'template' );
1193 $templates[$alias] = $this->stripBom(
$content );
1217 private function expandPackageFiles(
Context $context ) {
1219 if ( isset( $this->expandedPackageFiles[$hash] ) ) {
1220 return $this->expandedPackageFiles[$hash];
1222 if ( $this->packageFiles ===
null ) {
1225 $expandedFiles = [];
1228 foreach ( $this->packageFiles as $key => $fileInfo ) {
1229 $expanded = $this->expandFileInfo( $context, $fileInfo,
"packageFiles[$key]" );
1230 $fileName = $expanded[
'name'];
1231 if ( !empty( $expanded[
'main'] ) ) {
1232 unset( $expanded[
'main'] );
1233 $type = $expanded[
'type'];
1234 $mainFile = $fileName;
1235 if (
$type !==
'script' &&
$type !==
'script-vue' ) {
1236 $msg =
"Main file in package must be of type 'script', module " .
1237 "'{$this->getName()}', main file '{$mainFile}' is '{$type}'.";
1238 $this->getLogger()->error( $msg );
1239 throw new LogicException( $msg );
1242 $expandedFiles[$fileName] = $expanded;
1245 if ( $expandedFiles && $mainFile ===
null ) {
1247 foreach ( $expandedFiles as
$path =>
$file ) {
1248 if (
$file[
'type'] ===
'script' ||
$file[
'type'] ===
'script-vue' ) {
1256 'main' => $mainFile,
1257 'files' => $expandedFiles
1260 $this->expandedPackageFiles[$hash] = $result;
1286 private function expandFileInfo( Context $context, $fileInfo, $debugKey ) {
1287 if ( is_string( $fileInfo ) ) {
1290 'name' => $fileInfo,
1291 'type' => self::getPackageFileType( $fileInfo ),
1292 'filePath' =>
new FilePath( $fileInfo, $this->localBasePath, $this->remoteBasePath )
1294 } elseif ( $fileInfo instanceof FilePath ) {
1296 'name' => $fileInfo->getPath(),
1299 } elseif ( !is_array( $fileInfo ) ) {
1300 $msg =
"Invalid type in $debugKey for module '{$this->getName()}', " .
1301 "must be array, string or FilePath";
1302 $this->getLogger()->error( $msg );
1303 throw new LogicException( $msg );
1305 if ( !isset( $fileInfo[
'name'] ) ) {
1306 $msg =
"Missing 'name' key in $debugKey for module '{$this->getName()}'";
1307 $this->getLogger()->error( $msg );
1308 throw new LogicException( $msg );
1310 $fileName = $this->getPath( $fileInfo[
'name'] );
1313 $type = $fileInfo[
'type'] ?? self::getPackageFileType( $fileName );
1315 'name' => $fileName,
1318 if ( !empty( $fileInfo[
'main'] ) ) {
1319 $expanded[
'main'] =
true;
1326 if ( isset( $fileInfo[
'content'] ) ) {
1327 $expanded[
'content'] = $fileInfo[
'content'];
1328 } elseif ( isset( $fileInfo[
'file'] ) ) {
1329 $expanded[
'filePath'] = $this->makeFilePath( $fileInfo[
'file'] );
1330 } elseif ( isset( $fileInfo[
'callback'] ) ) {
1332 $expanded[
'callbackParam'] = $fileInfo[
'callbackParam'] ??
null;
1334 if ( !is_callable( $fileInfo[
'callback'] ) ) {
1335 $msg =
"Invalid 'callback' for module '{$this->getName()}', file '{$fileName}'.";
1336 $this->getLogger()->error( $msg );
1337 throw new LogicException( $msg );
1339 if ( isset( $fileInfo[
'versionCallback'] ) ) {
1340 if ( !is_callable( $fileInfo[
'versionCallback'] ) ) {
1341 throw new LogicException(
"Invalid 'versionCallback' for "
1342 .
"module '{$this->getName()}', file '{$fileName}'."
1348 $callbackResult = ( $fileInfo[
'versionCallback'] )(
1351 $expanded[
'callbackParam']
1353 if ( $callbackResult instanceof FilePath ) {
1354 $callbackResult->initBasePaths( $this->localBasePath, $this->remoteBasePath );
1355 $expanded[
'filePath'] = $callbackResult;
1357 $expanded[
'definitionSummary'] = $callbackResult;
1360 $expanded[
'callback'] = $fileInfo[
'callback'];
1363 $callbackResult = ( $fileInfo[
'callback'] )(
1366 $expanded[
'callbackParam']
1368 if ( $callbackResult instanceof FilePath ) {
1369 $callbackResult->initBasePaths( $this->localBasePath, $this->remoteBasePath );
1370 $expanded[
'filePath'] = $callbackResult;
1372 $expanded[
'content'] = $callbackResult;
1375 } elseif ( isset( $fileInfo[
'config'] ) ) {
1376 if (
$type !==
'data' ) {
1377 $msg =
"Key 'config' only valid for data files. "
1378 .
" Module '{$this->getName()}', file '{$fileName}' is '{$type}'.";
1379 $this->getLogger()->error( $msg );
1380 throw new LogicException( $msg );
1382 $expandedConfig = [];
1383 foreach ( $fileInfo[
'config'] as $configKey => $var ) {
1384 $expandedConfig[ is_numeric( $configKey ) ? $var : $configKey ] = $this->getConfig()->get( $var );
1386 $expanded[
'content'] = $expandedConfig;
1387 } elseif ( !empty( $fileInfo[
'main'] ) ) {
1389 $expanded[
'filePath'] = $this->makeFilePath( $fileName );
1391 $msg =
"Incomplete definition for module '{$this->getName()}', file '{$fileName}'. "
1392 .
"One of 'file', 'content', 'callback', or 'config' must be set.";
1393 $this->getLogger()->error( $msg );
1394 throw new LogicException( $msg );
1405 private function makeFilePath(
$path ): FilePath {
1406 if (
$path instanceof FilePath ) {
1408 } elseif ( is_string(
$path ) ) {
1409 return new FilePath(
$path, $this->localBasePath, $this->remoteBasePath );
1411 throw new InvalidArgumentException(
'$path must be either FilePath or string' );
1422 if ( $this->packageFiles ===
null ) {
1426 if ( isset( $this->fullyExpandedPackageFiles[ $hash ] ) ) {
1427 return $this->fullyExpandedPackageFiles[ $hash ];
1429 $expandedPackageFiles = $this->expandPackageFiles( $context ) ?? [];
1431 foreach ( $expandedPackageFiles[
'files'] as &$fileInfo ) {
1432 $this->readFileInfo( $context, $fileInfo );
1435 $this->fullyExpandedPackageFiles[ $hash ] = $expandedPackageFiles;
1436 return $expandedPackageFiles;
1447 private function readFileInfo(
Context $context, array &$fileInfo ) {
1452 if ( !isset( $fileInfo[
'content'] ) && isset( $fileInfo[
'callback'] ) ) {
1453 $callbackResult = ( $fileInfo[
'callback'] )(
1456 $fileInfo[
'callbackParam']
1458 if ( $callbackResult instanceof
FilePath ) {
1460 $fileInfo[
'filePath'] = $callbackResult;
1462 $fileInfo[
'content'] = $callbackResult;
1464 unset( $fileInfo[
'callback'] );
1471 if ( !isset( $fileInfo[
'content'] ) && isset( $fileInfo[
'filePath'] ) ) {
1472 $localPath = $this->getLocalPath( $fileInfo[
'filePath'] );
1473 $content = $this->getFileContents( $localPath,
'package' );
1474 if ( $fileInfo[
'type'] ===
'data' ) {
1479 if ( $fileInfo[
'type'] ===
'script-vue' ) {
1481 $parsedComponent = $this->getVueComponentParser()->parse(
1483 $fileInfo[
'content'],
1484 [
'minifyTemplate' => !$context->
getDebug() ]
1486 }
catch ( TimeoutException $e ) {
1488 }
catch ( Exception $e ) {
1489 $msg =
"Error parsing file '{$fileInfo['name']}' in module '{$this->getName()}': " .
1491 $this->getLogger()->error( $msg );
1492 throw new RuntimeException( $msg );
1494 $encodedTemplate = json_encode( $parsedComponent[
'template'] );
1498 $encodedTemplate = preg_replace(
'/(?<!\\\\)\\\\n/',
" \\\n", $encodedTemplate );
1500 $encodedTemplate = strtr( $encodedTemplate, [
"\\t" =>
"\t" ] );
1502 $fileInfo[
'content'] = [
1503 'script' => $parsedComponent[
'script'] .
1504 ";\nmodule.exports.template = $encodedTemplate;",
1505 'style' => $parsedComponent[
'style'] ??
'',
1506 'styleLang' => $parsedComponent[
'styleLang'] ??
'css'
1508 $fileInfo[
'type'] =
'script+style';
1510 if ( !isset( $fileInfo[
'content'] ) ) {
1512 $msg =
"Unable to resolve contents for file {$fileInfo['name']}";
1513 $this->getLogger()->error( $msg );
1514 throw new RuntimeException( $msg );
1518 unset( $fileInfo[
'definitionSummary'] );
1520 unset( $fileInfo[
'callbackParam'] );
1534 if ( str_starts_with( $input,
"\xef\xbb\xbf" ) ) {
1535 return substr( $input, 3 );
1542 class_alias( FileModule::class,
'ResourceLoaderFileModule' );
if(!defined( 'MEDIAWIKI')) if(ini_get( 'mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
if(!defined('MW_SETUP_CALLBACK'))
Load JSON files, and uses a Processor to extract information.
static getFileContentsHash( $filePaths)
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
A class containing constants representing the names of configuration variables.
const StylePath
Name constant for the StylePath setting, for use with Config::get()
const ExtensionAssetsPath
Name constant for the ExtensionAssetsPath setting, for use with Config::get()
const Server
Name constant for the Server setting, for use with Config::get()
const ResourceBasePath
Name constant for the ResourceBasePath setting, for use with Config::get()
Context object that contains information about the state of a specific ResourceLoader web request.
getHash()
All factors that uniquely identify this request, except 'modules'.
Functions to get cache objects.
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from configuration)
This is one of the Core classes and should be read at least once by any new developers.
static transformResourcePath(Config $config, $path)
Transform path to web-accessible static resource.
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