29use InvalidArgumentException;
37use Wikimedia\Minify\CSSMin;
38use Wikimedia\RequestTimeout\TimeoutException;
103 private $expandedPackageFiles = [];
109 private $fullyExpandedPackageFiles = [];
181 $hasTemplates =
false;
184 list( $this->localBasePath, $this->remoteBasePath ) =
188 foreach ( $options as $member => $option ) {
195 $this->{$member} = is_array( $option ) ? $option : [ $option ];
198 $hasTemplates =
true;
199 $this->{$member} = is_array( $option ) ? $option : [ $option ];
202 case 'languageScripts':
205 if ( !is_array( $option ) ) {
206 throw new InvalidArgumentException(
207 "Invalid collated file path list error. " .
208 "'$option' given, array expected."
211 foreach ( $option as $key => $value ) {
212 if ( !is_string( $key ) ) {
213 throw new InvalidArgumentException(
214 "Invalid collated file path list key error. " .
215 "'$key' given, string expected."
218 $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
222 $this->deprecated = $option;
229 $option = array_values( array_unique( (array)$option ) );
232 $this->{$member} = $option;
237 $this->{$member} = (string)$option;
243 $this->{$member} = (bool)$option;
247 if ( isset( $options[
'scripts'] ) && isset( $options[
'packageFiles'] ) ) {
248 throw new InvalidArgumentException(
"A module may not set both 'scripts' and 'packageFiles'" );
250 if ( isset( $options[
'packageFiles'] ) && isset( $options[
'skinScripts'] ) ) {
251 throw new InvalidArgumentException(
"Options 'skinScripts' and 'packageFiles' cannot be used together." );
253 if ( $hasTemplates ) {
254 $this->dependencies[] =
'mediawiki.template';
256 foreach ( $this->templates as $alias => $templatePath ) {
257 if ( is_int( $alias ) ) {
258 $alias = $this->
getPath( $templatePath );
260 $suffix = explode(
'.', $alias );
261 $suffix = end( $suffix );
262 $compilerModule =
'mediawiki.template.' . $suffix;
263 if ( $suffix !==
'html' && !in_array( $compilerModule, $this->dependencies ) ) {
264 $this->dependencies[] = $compilerModule;
298 if ( isset( $options[
'remoteExtPath'] ) ) {
301 $remoteBasePath = $extensionAssetsPath .
'/' . $options[
'remoteExtPath'];
304 if ( isset( $options[
'remoteSkinPath'] ) ) {
310 if ( array_key_exists(
'localBasePath', $options ) ) {
314 if ( array_key_exists(
'remoteBasePath', $options ) ) {
344 if (
$file[
'type'] ===
'script+style' ) {
345 $file[
'content'] =
$file[
'content'][
'script'];
346 $file[
'type'] =
'script';
349 if ( $deprecationScript ) {
351 $mainFile[
'content'] = $deprecationScript . $mainFile[
'content'];
356 $files = $this->getScriptFiles( $context );
357 return $deprecationScript . $this->readScriptFiles( $files );
370 foreach ( $this->getScriptFiles( $context ) as
$file ) {
373 $url = $rl->expandUrl( $server, $url );
403 if (
$file[
'type'] ===
'script+style' ) {
405 $file[
'content'][
'style'],
406 $file[
'content'][
'styleLang'],
427 if ( $this->hasGeneratedStyles ) {
430 return parent::getStyleURLsForDebug( $context );
435 foreach ( $this->
getStyleFiles( $context ) as $mediaType => $list ) {
436 $urls[$mediaType] = [];
437 foreach ( $list as
$file ) {
438 $urls[$mediaType][] = OutputPage::transformResourcePath(
482 private function getFileContents( $localPath,
$type ) {
483 if ( !is_file( $localPath ) ) {
484 throw new RuntimeException(
"$type file not found or not a file: \"$localPath\"" );
486 return $this->
stripBom( file_get_contents( $localPath ) );
493 if ( !$this->skipFunction ) {
496 $localPath = $this->
getLocalPath( $this->skipFunction );
497 return $this->getFileContents( $localPath,
'skip function' );
522 private function getFileHashes(
Context $context ) {
526 foreach ( $styleFiles as $paths ) {
527 $files = array_merge( $files, $paths );
533 $expandedPackageFiles = $this->expandPackageFiles( $context );
535 if ( $expandedPackageFiles ) {
536 foreach ( $expandedPackageFiles[
'files'] as $fileInfo ) {
537 if ( isset( $fileInfo[
'filePath'] ) ) {
545 $files = array_merge(
550 $context->
getDebug() ? $this->debugScripts : [],
551 $this->getLanguageScripts( $context->getLanguage() ),
552 self::
tryForKey( $this->skinScripts, $context->getSkin(),
'default' )
554 if ( $this->skipFunction ) {
559 $files = array_map( [ $this,
'getLocalPath' ], $files );
567 $files = array_unique( $files );
583 $summary = parent::getDefinitionSummary( $context );
605 $options[$member] = $this->{$member};
618 $packageFiles[
'files'] = array_map(
static function ( $fileInfo ) {
619 return $fileInfo[
'definitionSummary'] ?? ( $fileInfo[
'content'] ?? null );
624 'options' => $options,
626 'fileHashes' => $this->getFileHashes( $context ),
632 $summary[] = [
'lessVars' => $lessVars ];
642 if ( $this->vueComponentParser ===
null ) {
654 return $path->getPath();
666 if (
$path->getLocalBasePath() !==
null ) {
667 return $path->getLocalPath();
672 return "{$this->localBasePath}/$path";
681 if (
$path->getRemoteBasePath() !==
null ) {
682 return $path->getRemotePath();
687 if ( $this->remoteBasePath ===
'/' ) {
690 return "{$this->remoteBasePath}/$path";
702 return preg_match(
'/\.less$/i',
$path ) ?
'less' :
'css';
711 if ( preg_match(
'/\.json$/i',
$path ) ) {
714 if ( preg_match(
'/\.vue$/i',
$path ) ) {
727 private static function collateStyleFilesByMedia( array $list ) {
729 foreach ( $list as $key => $value ) {
730 if ( is_int( $key ) ) {
732 if ( !isset( $collatedFiles[
'all'] ) ) {
733 $collatedFiles[
'all'] = [];
735 $collatedFiles[
'all'][] = $value;
736 } elseif ( is_array( $value ) ) {
738 $optionValue = $value[
'media'] ??
'all';
739 if ( !isset( $collatedFiles[$optionValue] ) ) {
740 $collatedFiles[$optionValue] = [];
742 $collatedFiles[$optionValue][] = $key;
745 return $collatedFiles;
758 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
775 private function getScriptFiles(
Context $context ): array {
778 $files = array_merge(
780 $this->getLanguageScripts( $context->getLanguage() ),
781 self::
tryForKey( $this->skinScripts, $context->getSkin(),
'default' )
784 $files = array_merge( $files, $this->debugScripts );
787 return array_unique( $files, SORT_REGULAR );
797 private function getLanguageScripts(
string $lang ): array {
798 $scripts = self::tryForKey( $this->languageScripts,
$lang );
805 if ( $this->languageScripts ) {
807 ->getLanguageFallback()
808 ->getAll(
$lang, LanguageFallback::MESSAGES );
809 foreach ( $fallbacks as
$lang ) {
810 $scripts = self::tryForKey( $this->languageScripts,
$lang );
821 $moduleName = $this->getName();
822 foreach ( $moduleSkinStyles as $skinName => $overrides ) {
825 if ( isset( $this->skinStyles[$skinName] ) ) {
831 if ( isset( $overrides[$moduleName] ) ) {
832 $paths = (array)$overrides[$moduleName];
834 } elseif ( isset( $overrides[
'+' . $moduleName] ) ) {
835 $paths = (array)$overrides[
'+' . $moduleName];
836 $styleFiles = isset( $this->skinStyles[
'default'] ) ?
837 (array)$this->skinStyles[
'default'] :
845 list( $localBasePath, $remoteBasePath ) = self::extractBasePaths( $overrides );
847 foreach ( $paths as
$path ) {
848 $styleFiles[] =
new FilePath(
$path, $localBasePath, $remoteBasePath );
851 $this->skinStyles[$skinName] = $styleFiles;
863 return array_merge_recursive(
864 self::collateStyleFilesByMedia( $this->styles ),
865 self::collateStyleFilesByMedia(
866 self::tryForKey( $this->skinStyles, $context->
getSkin(),
'default' )
879 return self::collateStyleFilesByMedia(
880 self::tryForKey( $this->skinStyles, $skinName )
891 $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
894 $internalSkinNames = array_keys( $skinFactory->getInstalledSkins() );
895 $internalSkinNames[] =
'default';
897 foreach ( $internalSkinNames as $internalSkinName ) {
898 $styleFiles = array_merge_recursive(
900 $this->getSkinStyleFiles( $internalSkinName )
913 $collatedStyleFiles = array_merge_recursive(
914 self::collateStyleFilesByMedia( $this->styles ),
915 $this->getAllSkinStyleFiles()
920 foreach ( $collatedStyleFiles as $media => $styleFiles ) {
921 foreach ( $styleFiles as $styleFile ) {
922 $result[] = $this->getLocalPath( $styleFile );
935 private function readScriptFiles( array $scripts ) {
940 foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
941 $localPath = $this->getLocalPath( $fileName );
942 $contents = $this->getFileContents( $localPath,
'script' );
943 $js .= ResourceLoader::ensureNewline( $contents );
960 foreach ( $styles as $media => $files ) {
961 $uniqueFiles = array_unique( $files, SORT_REGULAR );
963 foreach ( $uniqueFiles as
$file ) {
964 $styleFiles[] = $this->readStyleFile(
$file, $context );
966 $styles[$media] = implode(
"\n", $styleFiles );
982 $localPath = $this->getLocalPath(
$path );
983 $style = $this->getFileContents( $localPath,
'style' );
984 $styleLang = $this->getStyleSheetLang( $localPath );
986 return $this->processStyle( $style, $styleLang,
$path, $context );
1006 $localPath = $this->getLocalPath(
$path );
1007 $remotePath = $this->getRemotePath(
$path );
1009 if ( $styleLang ===
'less' ) {
1010 $style = $this->compileLessString( $style, $localPath, $context );
1011 $this->hasGeneratedStyles =
true;
1014 if ( $this->getFlip( $context ) ) {
1015 $style = CSSJanus::transform(
1022 $localDir = dirname( $localPath );
1023 $remoteDir = dirname( $remotePath );
1025 $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
1026 foreach ( $localFileRefs as
$file ) {
1027 if ( is_file(
$file ) ) {
1028 $this->localFileRefs[] =
$file;
1030 $this->missingLocalFileRefs[] =
$file;
1035 return CSSMin::remap( $style, $localDir, $remoteDir,
true );
1044 return $context->
getDirection() ===
'rtl' && !$this->noflip;
1053 return $this->targets;
1063 $canBeStylesOnly = !(
1066 || $this->debugScripts
1068 || $this->languageScripts
1069 || $this->skinScripts
1070 || $this->dependencies
1072 || $this->skipFunction
1073 || $this->packageFiles
1075 return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
1096 $skinName = $context->
getSkin();
1097 $skinImportPaths = ExtensionRegistry::getInstance()->getAttribute(
'SkinLessImportPaths' );
1099 if ( isset( $skinImportPaths[ $skinName ] ) ) {
1100 $importDirs[] = $skinImportPaths[ $skinName ];
1103 $vars = $this->getLessVars( $context );
1109 'importDirs' => $importDirs,
1111 $key =
$cache->makeGlobalKey(
1112 'resourceloader-less',
1114 hash(
'md4', $style ),
1115 hash(
'md4',
serialize( $compilerParams ) )
1120 $data =
$cache->get( $key );
1123 $data[
'hash'] !== FileContentsHasher::getFileContentsHash( $data[
'files'] )
1125 $compiler = $context->
getResourceLoader()->getLessCompiler( $vars, $importDirs );
1127 $css = $compiler->parse( $style, $stylePath )->getCss();
1131 $files = $compiler->AllParsedFiles();
1134 'files' => Module::getRelativePaths( $files ),
1135 'hash' => FileContentsHasher::getFileContentsHash( $files )
1137 $cache->set( $key, $data, $cache::TTL_DAY );
1140 foreach ( Module::expandRelativePaths( $data[
'files'] ) as
$path ) {
1141 $this->localFileRefs[] =
$path;
1144 return $data[
'css'];
1155 foreach ( $this->templates as $alias => $templatePath ) {
1157 if ( is_int( $alias ) ) {
1158 $alias = $this->getPath( $templatePath );
1160 $localPath = $this->getLocalPath( $templatePath );
1161 $content = $this->getFileContents( $localPath,
'template' );
1163 $templates[$alias] = $this->stripBom(
$content );
1186 private function expandPackageFiles(
Context $context ) {
1188 if ( isset( $this->expandedPackageFiles[$hash] ) ) {
1189 return $this->expandedPackageFiles[$hash];
1191 if ( $this->packageFiles ===
null ) {
1194 $expandedFiles = [];
1197 foreach ( $this->packageFiles as $key => $fileInfo ) {
1198 if ( !is_array( $fileInfo ) ) {
1199 $fileInfo = [
'name' => $fileInfo,
'file' => $fileInfo ];
1201 if ( !isset( $fileInfo[
'name'] ) ) {
1202 $msg =
"Missing 'name' key in package file info for module '{$this->getName()}'," .
1203 " offset '{$key}'.";
1204 $this->getLogger()->error( $msg );
1205 throw new LogicException( $msg );
1207 $fileName = $this->getPath( $fileInfo[
'name'] );
1210 $type = $fileInfo[
'type'] ?? self::getPackageFileType( $fileName );
1211 $expanded = [
'type' =>
$type ];
1212 if ( !empty( $fileInfo[
'main'] ) ) {
1213 $mainFile = $fileName;
1214 if (
$type !==
'script' &&
$type !==
'script-vue' ) {
1215 $msg =
"Main file in package must be of type 'script', module " .
1216 "'{$this->getName()}', main file '{$mainFile}' is '{$type}'.";
1217 $this->getLogger()->error( $msg );
1218 throw new LogicException( $msg );
1226 if ( isset( $fileInfo[
'content'] ) ) {
1227 $expanded[
'content'] = $fileInfo[
'content'];
1228 } elseif ( isset( $fileInfo[
'file'] ) ) {
1229 $expanded[
'filePath'] = $fileInfo[
'file'];
1230 } elseif ( isset( $fileInfo[
'callback'] ) ) {
1232 $expanded[
'callbackParam'] = $fileInfo[
'callbackParam'] ??
null;
1234 if ( !is_callable( $fileInfo[
'callback'] ) ) {
1235 $msg =
"Invalid 'callback' for module '{$this->getName()}', file '{$fileName}'.";
1236 $this->getLogger()->error( $msg );
1237 throw new LogicException( $msg );
1239 if ( isset( $fileInfo[
'versionCallback'] ) ) {
1240 if ( !is_callable( $fileInfo[
'versionCallback'] ) ) {
1241 throw new LogicException(
"Invalid 'versionCallback' for "
1242 .
"module '{$this->getName()}', file '{$fileName}'."
1248 $callbackResult = ( $fileInfo[
'versionCallback'] )(
1251 $expanded[
'callbackParam']
1253 if ( $callbackResult instanceof FilePath ) {
1254 $expanded[
'filePath'] = $callbackResult;
1256 $expanded[
'definitionSummary'] = $callbackResult;
1259 $expanded[
'callback'] = $fileInfo[
'callback'];
1262 $callbackResult = ( $fileInfo[
'callback'] )(
1265 $expanded[
'callbackParam']
1267 if ( $callbackResult instanceof FilePath ) {
1268 $expanded[
'filePath'] = $callbackResult;
1270 $expanded[
'content'] = $callbackResult;
1273 } elseif ( isset( $fileInfo[
'config'] ) ) {
1274 if (
$type !==
'data' ) {
1275 $msg =
"Key 'config' only valid for data files. "
1276 .
" Module '{$this->getName()}', file '{$fileName}' is '{$type}'.";
1277 $this->getLogger()->error( $msg );
1278 throw new LogicException( $msg );
1280 $expandedConfig = [];
1281 foreach ( $fileInfo[
'config'] as $configKey => $var ) {
1282 $expandedConfig[ is_numeric( $configKey ) ? $var : $configKey ] = $this->getConfig()->get( $var );
1284 $expanded[
'content'] = $expandedConfig;
1285 } elseif ( !empty( $fileInfo[
'main'] ) ) {
1287 $expanded[
'filePath'] = $fileName;
1289 $msg =
"Incomplete definition for module '{$this->getName()}', file '{$fileName}'. "
1290 .
"One of 'file', 'content', 'callback', or 'config' must be set.";
1291 $this->getLogger()->error( $msg );
1292 throw new LogicException( $msg );
1295 $expandedFiles[$fileName] = $expanded;
1298 if ( $expandedFiles && $mainFile ===
null ) {
1300 foreach ( $expandedFiles as
$path =>
$file ) {
1301 if (
$file[
'type'] ===
'script' ||
$file[
'type'] ===
'script-vue' ) {
1309 'main' => $mainFile,
1310 'files' => $expandedFiles
1313 $this->expandedPackageFiles[$hash] = $result;
1324 if ( $this->packageFiles ===
null ) {
1328 if ( isset( $this->fullyExpandedPackageFiles[ $hash ] ) ) {
1329 return $this->fullyExpandedPackageFiles[ $hash ];
1331 $expandedPackageFiles = $this->expandPackageFiles( $context );
1334 foreach ( $expandedPackageFiles[
'files'] as $fileName => &$fileInfo ) {
1339 if ( isset( $fileInfo[
'callback'] ) ) {
1340 $callbackResult = ( $fileInfo[
'callback'] )(
1343 $fileInfo[
'callbackParam']
1345 if ( $callbackResult instanceof
FilePath ) {
1347 $fileInfo[
'filePath'] = $callbackResult;
1349 $fileInfo[
'content'] = $callbackResult;
1351 unset( $fileInfo[
'callback'] );
1358 if ( !isset( $fileInfo[
'content'] ) && isset( $fileInfo[
'filePath'] ) ) {
1359 $localPath = $this->getLocalPath( $fileInfo[
'filePath'] );
1360 $content = $this->getFileContents( $localPath,
'package' );
1361 if ( $fileInfo[
'type'] ===
'data' ) {
1365 unset( $fileInfo[
'filePath'] );
1367 if ( $fileInfo[
'type'] ===
'script-vue' ) {
1369 $parsedComponent = $this->getVueComponentParser()->parse(
1371 $fileInfo[
'content'],
1372 [
'minifyTemplate' => !$context->
getDebug() ]
1374 }
catch ( TimeoutException $e ) {
1376 }
catch ( Exception $e ) {
1377 $msg =
"Error parsing file '$fileName' in module '{$this->getName()}': " .
1379 $this->getLogger()->error( $msg );
1380 throw new RuntimeException( $msg );
1382 $encodedTemplate = json_encode( $parsedComponent[
'template'] );
1386 $encodedTemplate = preg_replace(
'/(?<!\\\\)\\\\n/',
" \\\n", $encodedTemplate );
1388 $encodedTemplate = strtr( $encodedTemplate, [
"\\t" =>
"\t" ] );
1390 $fileInfo[
'content'] = [
1391 'script' => $parsedComponent[
'script'] .
1392 ";\nmodule.exports.template = $encodedTemplate;",
1393 'style' => $parsedComponent[
'style'] ??
'',
1394 'styleLang' => $parsedComponent[
'styleLang'] ??
'css'
1396 $fileInfo[
'type'] =
'script+style';
1400 unset( $fileInfo[
'definitionSummary'] );
1402 unset( $fileInfo[
'callbackParam'] );
1405 $this->fullyExpandedPackageFiles[ $hash ] = $expandedPackageFiles;
1406 return $expandedPackageFiles;
1420 if ( str_starts_with( $input,
"\xef\xbb\xbf" ) ) {
1421 return substr( $input, 3 );
1428class_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