29use InvalidArgumentException;
37use Wikimedia\Minify\CSSMin;
38use Wikimedia\RequestTimeout\TimeoutException;
103 private $expandedPackageFiles = [];
109 private $fullyExpandedPackageFiles = [];
178 $hasTemplates =
false;
185 foreach ( $options as $member => $option ) {
192 $this->{$member} = is_array( $option ) ? $option : [ $option ];
195 $hasTemplates =
true;
196 $this->{$member} = is_array( $option ) ? $option : [ $option ];
199 case 'languageScripts':
202 if ( !is_array( $option ) ) {
203 throw new InvalidArgumentException(
204 "Invalid collated file path list error. " .
205 "'$option' given, array expected."
208 foreach ( $option as $key => $value ) {
209 if ( !is_string( $key ) ) {
210 throw new InvalidArgumentException(
211 "Invalid collated file path list key error. " .
212 "'$key' given, string expected."
215 $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
219 $this->deprecated = $option;
226 $option = array_values( array_unique( (array)$option ) );
229 $this->{$member} = $option;
234 $this->{$member} = (string)$option;
240 $this->{$member} = (bool)$option;
244 if ( isset( $options[
'scripts'] ) && isset( $options[
'packageFiles'] ) ) {
245 throw new InvalidArgumentException(
"A module may not set both 'scripts' and 'packageFiles'" );
247 if ( isset( $options[
'packageFiles'] ) && isset( $options[
'skinScripts'] ) ) {
248 throw new InvalidArgumentException(
"Options 'skinScripts' and 'packageFiles' cannot be used together." );
250 if ( $hasTemplates ) {
251 $this->dependencies[] =
'mediawiki.template';
253 foreach ( $this->templates as $alias => $templatePath ) {
254 if ( is_int( $alias ) ) {
255 $alias = $this->
getPath( $templatePath );
257 $suffix = explode(
'.', $alias );
258 $suffix = end( $suffix );
259 $compilerModule =
'mediawiki.template.' . $suffix;
260 if ( $suffix !==
'html' && !in_array( $compilerModule, $this->dependencies ) ) {
261 $this->dependencies[] = $compilerModule;
292 if ( isset( $options[
'remoteExtPath'] ) ) {
295 $remoteBasePath = $extensionAssetsPath .
'/' . $options[
'remoteExtPath'];
298 if ( isset( $options[
'remoteSkinPath'] ) ) {
304 if ( array_key_exists(
'localBasePath', $options ) ) {
308 if ( array_key_exists(
'remoteBasePath', $options ) ) {
338 if (
$file[
'type'] ===
'script+style' ) {
339 $file[
'content'] =
$file[
'content'][
'script'];
340 $file[
'type'] =
'script';
343 if ( $deprecationScript ) {
345 $mainFile[
'content'] = $deprecationScript . $mainFile[
'content'];
350 $files = $this->getScriptFiles( $context );
351 return $deprecationScript . $this->readScriptFiles( $files );
364 foreach ( $this->getScriptFiles( $context ) as
$file ) {
367 $url = $rl->expandUrl( $server, $url );
397 if (
$file[
'type'] ===
'script+style' ) {
399 $file[
'content'][
'style'],
400 $file[
'content'][
'styleLang'],
421 if ( $this->hasGeneratedStyles ) {
424 return parent::getStyleURLsForDebug( $context );
429 foreach ( $this->
getStyleFiles( $context ) as $mediaType => $list ) {
430 $urls[$mediaType] = [];
431 foreach ( $list as
$file ) {
432 $urls[$mediaType][] = OutputPage::transformResourcePath(
476 private function getFileContents( $localPath,
$type ) {
477 if ( !is_file( $localPath ) ) {
478 throw new RuntimeException(
"$type file not found or not a file: \"$localPath\"" );
480 return $this->
stripBom( file_get_contents( $localPath ) );
487 if ( !$this->skipFunction ) {
490 $localPath = $this->
getLocalPath( $this->skipFunction );
491 return $this->getFileContents( $localPath,
'skip function' );
516 private function getFileHashes(
Context $context ) {
520 foreach ( $styleFiles as $paths ) {
521 $files = array_merge( $files, $paths );
527 $expandedPackageFiles = $this->expandPackageFiles( $context );
529 if ( $expandedPackageFiles ) {
530 foreach ( $expandedPackageFiles[
'files'] as $fileInfo ) {
531 if ( isset( $fileInfo[
'filePath'] ) ) {
539 $files = array_merge(
544 $context->
getDebug() ? $this->debugScripts : [],
545 $this->getLanguageScripts( $context->getLanguage() ),
546 self::
tryForKey( $this->skinScripts, $context->getSkin(),
'default' )
548 if ( $this->skipFunction ) {
553 $files = array_map( [ $this,
'getLocalPath' ], $files );
561 $files = array_unique( $files );
577 $summary = parent::getDefinitionSummary( $context );
599 $options[$member] = $this->{$member};
612 $packageFiles[
'files'] = array_map(
static function ( $fileInfo ) {
613 return $fileInfo[
'definitionSummary'] ?? ( $fileInfo[
'content'] ?? null );
618 'options' => $options,
620 'fileHashes' => $this->getFileHashes( $context ),
626 $summary[] = [
'lessVars' => $lessVars ];
636 if ( $this->vueComponentParser ===
null ) {
648 return $path->getPath();
660 if (
$path->getLocalBasePath() !==
null ) {
661 return $path->getLocalPath();
666 return "{$this->localBasePath}/$path";
675 if (
$path->getRemoteBasePath() !==
null ) {
676 return $path->getRemotePath();
681 if ( $this->remoteBasePath ===
'/' ) {
684 return "{$this->remoteBasePath}/$path";
696 return preg_match(
'/\.less$/i',
$path ) ?
'less' :
'css';
705 if ( preg_match(
'/\.json$/i',
$path ) ) {
708 if ( preg_match(
'/\.vue$/i',
$path ) ) {
721 private static function collateStyleFilesByMedia( array $list ) {
723 foreach ( $list as $key => $value ) {
724 if ( is_int( $key ) ) {
726 $collatedFiles[
'all'][] = $value;
727 } elseif ( is_array( $value ) ) {
729 $optionValue = $value[
'media'] ??
'all';
730 $collatedFiles[$optionValue][] = $key;
733 return $collatedFiles;
746 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
763 private function getScriptFiles(
Context $context ): array {
766 $files = array_merge(
768 $this->getLanguageScripts( $context->getLanguage() ),
769 self::
tryForKey( $this->skinScripts, $context->getSkin(),
'default' )
772 $files = array_merge( $files, $this->debugScripts );
775 return array_unique( $files, SORT_REGULAR );
785 private function getLanguageScripts(
string $lang ): array {
786 $scripts = self::tryForKey( $this->languageScripts,
$lang );
793 if ( $this->languageScripts ) {
795 ->getLanguageFallback()
796 ->getAll(
$lang, LanguageFallback::MESSAGES );
797 foreach ( $fallbacks as
$lang ) {
798 $scripts = self::tryForKey( $this->languageScripts,
$lang );
809 $moduleName = $this->getName();
810 foreach ( $moduleSkinStyles as $skinName => $overrides ) {
813 if ( isset( $this->skinStyles[$skinName] ) ) {
819 if ( isset( $overrides[$moduleName] ) ) {
820 $paths = (array)$overrides[$moduleName];
822 } elseif ( isset( $overrides[
'+' . $moduleName] ) ) {
823 $paths = (array)$overrides[
'+' . $moduleName];
824 $styleFiles = isset( $this->skinStyles[
'default'] ) ?
825 (array)$this->skinStyles[
'default'] :
833 [ $localBasePath, $remoteBasePath ] = self::extractBasePaths( $overrides );
835 foreach ( $paths as
$path ) {
836 $styleFiles[] =
new FilePath(
$path, $localBasePath, $remoteBasePath );
839 $this->skinStyles[$skinName] = $styleFiles;
851 return array_merge_recursive(
852 self::collateStyleFilesByMedia( $this->styles ),
853 self::collateStyleFilesByMedia(
854 self::tryForKey( $this->skinStyles, $context->
getSkin(),
'default' )
867 return self::collateStyleFilesByMedia(
868 self::tryForKey( $this->skinStyles, $skinName )
879 $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
882 $internalSkinNames = array_keys( $skinFactory->getInstalledSkins() );
883 $internalSkinNames[] =
'default';
885 foreach ( $internalSkinNames as $internalSkinName ) {
886 $styleFiles = array_merge_recursive(
888 $this->getSkinStyleFiles( $internalSkinName )
901 $collatedStyleFiles = array_merge_recursive(
902 self::collateStyleFilesByMedia( $this->styles ),
903 $this->getAllSkinStyleFiles()
908 foreach ( $collatedStyleFiles as $styleFiles ) {
909 foreach ( $styleFiles as $styleFile ) {
910 $result[] = $this->getLocalPath( $styleFile );
923 private function readScriptFiles( array $scripts ) {
928 foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
929 $localPath = $this->getLocalPath( $fileName );
930 $contents = $this->getFileContents( $localPath,
'script' );
931 $js .= ResourceLoader::ensureNewline( $contents );
948 foreach ( $styles as $media => $files ) {
949 $uniqueFiles = array_unique( $files, SORT_REGULAR );
951 foreach ( $uniqueFiles as
$file ) {
952 $styleFiles[] = $this->readStyleFile(
$file, $context );
954 $styles[$media] = implode(
"\n", $styleFiles );
970 $localPath = $this->getLocalPath(
$path );
971 $style = $this->getFileContents( $localPath,
'style' );
972 $styleLang = $this->getStyleSheetLang( $localPath );
974 return $this->processStyle( $style, $styleLang,
$path, $context );
994 $localPath = $this->getLocalPath(
$path );
995 $remotePath = $this->getRemotePath(
$path );
997 if ( $styleLang ===
'less' ) {
998 $style = $this->compileLessString( $style, $localPath, $context );
999 $this->hasGeneratedStyles =
true;
1002 if ( $this->getFlip( $context ) ) {
1003 $style = CSSJanus::transform(
1010 $localDir = dirname( $localPath );
1011 $remoteDir = dirname( $remotePath );
1013 $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
1014 foreach ( $localFileRefs as
$file ) {
1015 if ( is_file(
$file ) ) {
1016 $this->localFileRefs[] =
$file;
1018 $this->missingLocalFileRefs[] =
$file;
1023 return CSSMin::remap( $style, $localDir, $remoteDir,
true );
1032 return $context->
getDirection() ===
'rtl' && !$this->noflip;
1041 return $this->targets;
1051 $canBeStylesOnly = !(
1054 || $this->debugScripts
1056 || $this->languageScripts
1057 || $this->skinScripts
1058 || $this->dependencies
1060 || $this->skipFunction
1061 || $this->packageFiles
1063 return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
1084 $skinName = $context->
getSkin();
1085 $skinImportPaths = ExtensionRegistry::getInstance()->getAttribute(
'SkinLessImportPaths' );
1087 if ( isset( $skinImportPaths[ $skinName ] ) ) {
1088 $importDirs[] = $skinImportPaths[ $skinName ];
1091 $vars = $this->getLessVars( $context );
1097 'importDirs' => $importDirs,
1099 $key = $cache->makeGlobalKey(
1100 'resourceloader-less',
1102 hash(
'md4', $style ),
1103 hash(
'md4', serialize( $compilerParams ) )
1108 $data = $cache->get( $key );
1111 $data[
'hash'] !== FileContentsHasher::getFileContentsHash( $data[
'files'] )
1113 $compiler = $context->
getResourceLoader()->getLessCompiler( $vars, $importDirs );
1115 $css = $compiler->parse( $style, $stylePath )->getCss();
1119 $files = $compiler->AllParsedFiles();
1122 'files' => Module::getRelativePaths( $files ),
1123 'hash' => FileContentsHasher::getFileContentsHash( $files )
1125 $cache->set( $key, $data, $cache::TTL_DAY );
1128 foreach ( Module::expandRelativePaths( $data[
'files'] ) as
$path ) {
1129 $this->localFileRefs[] =
$path;
1132 return $data[
'css'];
1143 foreach ( $this->templates as $alias => $templatePath ) {
1145 if ( is_int( $alias ) ) {
1146 $alias = $this->getPath( $templatePath );
1148 $localPath = $this->getLocalPath( $templatePath );
1149 $content = $this->getFileContents( $localPath,
'template' );
1151 $templates[$alias] = $this->stripBom(
$content );
1174 private function expandPackageFiles(
Context $context ) {
1176 if ( isset( $this->expandedPackageFiles[$hash] ) ) {
1177 return $this->expandedPackageFiles[$hash];
1179 if ( $this->packageFiles ===
null ) {
1182 $expandedFiles = [];
1185 foreach ( $this->packageFiles as $key => $fileInfo ) {
1186 if ( !is_array( $fileInfo ) ) {
1187 $fileInfo = [
'name' => $fileInfo,
'file' => $fileInfo ];
1189 if ( !isset( $fileInfo[
'name'] ) ) {
1190 $msg =
"Missing 'name' key in package file info for module '{$this->getName()}'," .
1191 " offset '{$key}'.";
1192 $this->getLogger()->error( $msg );
1193 throw new LogicException( $msg );
1195 $fileName = $this->getPath( $fileInfo[
'name'] );
1198 $type = $fileInfo[
'type'] ?? self::getPackageFileType( $fileName );
1199 $expanded = [
'type' =>
$type ];
1200 if ( !empty( $fileInfo[
'main'] ) ) {
1201 $mainFile = $fileName;
1202 if (
$type !==
'script' &&
$type !==
'script-vue' ) {
1203 $msg =
"Main file in package must be of type 'script', module " .
1204 "'{$this->getName()}', main file '{$mainFile}' is '{$type}'.";
1205 $this->getLogger()->error( $msg );
1206 throw new LogicException( $msg );
1214 if ( isset( $fileInfo[
'content'] ) ) {
1215 $expanded[
'content'] = $fileInfo[
'content'];
1216 } elseif ( isset( $fileInfo[
'file'] ) ) {
1217 $expanded[
'filePath'] = $fileInfo[
'file'];
1218 } elseif ( isset( $fileInfo[
'callback'] ) ) {
1220 $expanded[
'callbackParam'] = $fileInfo[
'callbackParam'] ??
null;
1222 if ( !is_callable( $fileInfo[
'callback'] ) ) {
1223 $msg =
"Invalid 'callback' for module '{$this->getName()}', file '{$fileName}'.";
1224 $this->getLogger()->error( $msg );
1225 throw new LogicException( $msg );
1227 if ( isset( $fileInfo[
'versionCallback'] ) ) {
1228 if ( !is_callable( $fileInfo[
'versionCallback'] ) ) {
1229 throw new LogicException(
"Invalid 'versionCallback' for "
1230 .
"module '{$this->getName()}', file '{$fileName}'."
1236 $callbackResult = ( $fileInfo[
'versionCallback'] )(
1239 $expanded[
'callbackParam']
1241 if ( $callbackResult instanceof FilePath ) {
1242 $expanded[
'filePath'] = $callbackResult;
1244 $expanded[
'definitionSummary'] = $callbackResult;
1247 $expanded[
'callback'] = $fileInfo[
'callback'];
1250 $callbackResult = ( $fileInfo[
'callback'] )(
1253 $expanded[
'callbackParam']
1255 if ( $callbackResult instanceof FilePath ) {
1256 $expanded[
'filePath'] = $callbackResult;
1258 $expanded[
'content'] = $callbackResult;
1261 } elseif ( isset( $fileInfo[
'config'] ) ) {
1262 if (
$type !==
'data' ) {
1263 $msg =
"Key 'config' only valid for data files. "
1264 .
" Module '{$this->getName()}', file '{$fileName}' is '{$type}'.";
1265 $this->getLogger()->error( $msg );
1266 throw new LogicException( $msg );
1268 $expandedConfig = [];
1269 foreach ( $fileInfo[
'config'] as $configKey => $var ) {
1270 $expandedConfig[ is_numeric( $configKey ) ? $var : $configKey ] = $this->getConfig()->get( $var );
1272 $expanded[
'content'] = $expandedConfig;
1273 } elseif ( !empty( $fileInfo[
'main'] ) ) {
1275 $expanded[
'filePath'] = $fileName;
1277 $msg =
"Incomplete definition for module '{$this->getName()}', file '{$fileName}'. "
1278 .
"One of 'file', 'content', 'callback', or 'config' must be set.";
1279 $this->getLogger()->error( $msg );
1280 throw new LogicException( $msg );
1283 $expandedFiles[$fileName] = $expanded;
1286 if ( $expandedFiles && $mainFile ===
null ) {
1288 foreach ( $expandedFiles as
$path =>
$file ) {
1289 if (
$file[
'type'] ===
'script' ||
$file[
'type'] ===
'script-vue' ) {
1297 'main' => $mainFile,
1298 'files' => $expandedFiles
1301 $this->expandedPackageFiles[$hash] = $result;
1312 if ( $this->packageFiles ===
null ) {
1316 if ( isset( $this->fullyExpandedPackageFiles[ $hash ] ) ) {
1317 return $this->fullyExpandedPackageFiles[ $hash ];
1319 $expandedPackageFiles = $this->expandPackageFiles( $context );
1322 foreach ( $expandedPackageFiles[
'files'] as $fileName => &$fileInfo ) {
1327 if ( isset( $fileInfo[
'callback'] ) ) {
1328 $callbackResult = ( $fileInfo[
'callback'] )(
1331 $fileInfo[
'callbackParam']
1333 if ( $callbackResult instanceof
FilePath ) {
1335 $fileInfo[
'filePath'] = $callbackResult;
1337 $fileInfo[
'content'] = $callbackResult;
1339 unset( $fileInfo[
'callback'] );
1346 if ( !isset( $fileInfo[
'content'] ) && isset( $fileInfo[
'filePath'] ) ) {
1347 $localPath = $this->getLocalPath( $fileInfo[
'filePath'] );
1348 $content = $this->getFileContents( $localPath,
'package' );
1349 if ( $fileInfo[
'type'] ===
'data' ) {
1353 unset( $fileInfo[
'filePath'] );
1355 if ( $fileInfo[
'type'] ===
'script-vue' ) {
1357 $parsedComponent = $this->getVueComponentParser()->parse(
1359 $fileInfo[
'content'],
1360 [
'minifyTemplate' => !$context->
getDebug() ]
1362 }
catch ( TimeoutException $e ) {
1364 }
catch ( Exception $e ) {
1365 $msg =
"Error parsing file '$fileName' in module '{$this->getName()}': " .
1367 $this->getLogger()->error( $msg );
1368 throw new RuntimeException( $msg );
1370 $encodedTemplate = json_encode( $parsedComponent[
'template'] );
1374 $encodedTemplate = preg_replace(
'/(?<!\\\\)\\\\n/',
" \\\n", $encodedTemplate );
1376 $encodedTemplate = strtr( $encodedTemplate, [
"\\t" =>
"\t" ] );
1378 $fileInfo[
'content'] = [
1379 'script' => $parsedComponent[
'script'] .
1380 ";\nmodule.exports.template = $encodedTemplate;",
1381 'style' => $parsedComponent[
'style'] ??
'',
1382 'styleLang' => $parsedComponent[
'styleLang'] ??
'css'
1384 $fileInfo[
'type'] =
'script+style';
1388 unset( $fileInfo[
'definitionSummary'] );
1390 unset( $fileInfo[
'callbackParam'] );
1393 $this->fullyExpandedPackageFiles[ $hash ] = $expandedPackageFiles;
1394 return $expandedPackageFiles;
1408 if ( str_starts_with( $input,
"\xef\xbb\xbf" ) ) {
1409 return substr( $input, 3 );
1416class_alias( FileModule::class,
'ResourceLoaderFileModule' );
if(!defined( 'MEDIAWIKI')) if(ini_get('mbstring.func_overload')) if(!defined( 'MW_ENTRY_POINT')) global $IP
Environment checks.
The Registry loads JSON files, and uses a Processor to extract information from them.
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.
This is one of the Core classes and should be read at least once by any new developers.
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