29 use InvalidArgumentException;
37 use Wikimedia\Minify\CSSMin;
38 use 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 ) {
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()
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();
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 );
1125 $compiler = $context->
getResourceLoader()->getLessCompiler( $vars, $importDirs );
1127 $css = $compiler->parse( $style, $stylePath )->getCss();
1131 $files = $compiler->AllParsedFiles();
1134 'files' => Module::getRelativePaths( $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 );
1428 class_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.
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