25use Wikimedia\Minify\CSSMin;
206 $hasTemplates =
false;
209 list( $this->localBasePath, $this->remoteBasePath ) =
213 foreach ( $options as $member => $option ) {
220 $this->{$member} = is_array( $option ) ? $option : [ $option ];
223 $hasTemplates =
true;
224 $this->{$member} = is_array( $option ) ? $option : [ $option ];
227 case 'languageScripts':
230 if ( !is_array( $option ) ) {
231 throw new InvalidArgumentException(
232 "Invalid collated file path list error. " .
233 "'$option' given, array expected."
236 foreach ( $option as $key => $value ) {
237 if ( !is_string( $key ) ) {
238 throw new InvalidArgumentException(
239 "Invalid collated file path list key error. " .
240 "'$key' given, string expected."
243 $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
247 $this->deprecated = $option;
254 $option = array_values( array_unique( (array)$option ) );
257 $this->{$member} = $option;
262 $this->{$member} = (string)$option;
268 $this->{$member} = (bool)$option;
272 if ( isset( $options[
'scripts'] ) && isset( $options[
'packageFiles'] ) ) {
273 throw new InvalidArgumentException(
"A module may not set both 'scripts' and 'packageFiles'" );
275 if ( isset( $options[
'packageFiles'] ) && isset( $options[
'skinScripts'] ) ) {
276 throw new InvalidArgumentException(
"Options 'skinScripts' and 'packageFiles' cannot be used together." );
278 if ( $hasTemplates ) {
279 $this->dependencies[] =
'mediawiki.template';
281 foreach ( $this->templates as $alias => $templatePath ) {
282 if ( is_int( $alias ) ) {
283 $alias = $this->
getPath( $templatePath );
285 $suffix = explode(
'.', $alias );
286 $suffix = end( $suffix );
287 $compilerModule =
'mediawiki.template.' . $suffix;
288 if ( $suffix !==
'html' && !in_array( $compilerModule, $this->dependencies ) ) {
289 $this->dependencies[] = $compilerModule;
323 if ( isset( $options[
'remoteExtPath'] ) ) {
328 if ( isset( $options[
'remoteSkinPath'] ) ) {
333 if ( array_key_exists(
'localBasePath', $options ) ) {
337 if ( array_key_exists(
'remoteBasePath', $options ) ) {
364 if ( $this->packageFiles !==
null ) {
367 if (
$file[
'type'] ===
'script+style' ) {
368 $file[
'content'] =
$file[
'content'][
'script'];
369 $file[
'type'] =
'script';
372 if ( $deprecationScript ) {
374 $mainFile[
'content'] = $deprecationScript . $mainFile[
'content'];
390 $urls[] = OutputPage::transformResourcePath(
419 if ( $this->packageFiles !==
null ) {
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 ) {
457 $urls[$mediaType][] = OutputPage::transformResourcePath(
502 if ( !is_file( $localPath ) ) {
503 throw new RuntimeException(
504 __METHOD__ .
": $type file not found, or is not a file: \"$localPath\""
507 return $this->
stripBom( file_get_contents( $localPath ) );
515 if ( !$this->skipFunction ) {
518 $localPath = $this->
getLocalPath( $this->skipFunction );
548 foreach ( $styleFiles as $paths ) {
549 $files = array_merge( $files, $paths );
559 if ( isset( $fileInfo[
'filePath'] ) ) {
567 $files = array_merge(
572 $context->
getDebug() ? $this->debugScripts : [],
576 if ( $this->skipFunction ) {
581 $files = array_map( [ $this,
'getLocalPath' ], $files );
589 $files = array_unique( $files );
605 $summary = parent::getDefinitionSummary( $context );
627 $options[$member] = $this->{$member};
640 $packageFiles[
'files'] = array_map(
static function ( $fileInfo ) {
641 return $fileInfo[
'definitionSummary'] ?? ( $fileInfo[
'content'] ?? null );
646 'options' => $options,
654 $summary[] = [
'lessVars' => $lessVars ];
664 if ( $this->vueComponentParser ===
null ) {
676 return $path->getPath();
688 return $path->getLocalPath();
691 return "{$this->localBasePath}/$path";
700 return $path->getRemotePath();
703 if ( $this->remoteBasePath ===
'/' ) {
706 return "{$this->remoteBasePath}/$path";
718 return preg_match(
'/\.less$/i',
$path ) ?
'less' :
'css';
727 if ( preg_match(
'/\.json$/i',
$path ) ) {
730 if ( preg_match(
'/\.vue$/i',
$path ) ) {
747 foreach ( $list as $key => $value ) {
748 if ( is_int( $key ) ) {
750 if ( !isset( $collatedFiles[$default] ) ) {
751 $collatedFiles[$default] = [];
753 $collatedFiles[$default][] = $value;
754 } elseif ( is_array( $value ) ) {
756 $optionValue = $value[$option] ?? $default;
757 if ( !isset( $collatedFiles[$optionValue] ) ) {
758 $collatedFiles[$optionValue] = [];
760 $collatedFiles[$optionValue][] = $key;
763 return $collatedFiles;
776 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
794 $files = array_merge(
797 self::tryForKey( $this->skinScripts, $context->
getSkin(),
'default' )
800 $files = array_merge( $files, $this->debugScripts );
803 return array_unique( $files, SORT_REGULAR );
818 $fallbacks = MediaWikiServices::getInstance()->getLanguageFallback()
819 ->getAll(
$lang, LanguageFallback::MESSAGES );
820 foreach ( $fallbacks as
$lang ) {
831 $moduleName = $this->
getName();
832 foreach ( $moduleSkinStyles as $skinName => $overrides ) {
835 if ( isset( $this->skinStyles[$skinName] ) ) {
841 if ( isset( $overrides[$moduleName] ) ) {
842 $paths = (array)$overrides[$moduleName];
844 } elseif ( isset( $overrides[
'+' . $moduleName] ) ) {
845 $paths = (array)$overrides[
'+' . $moduleName];
846 $styleFiles = isset( $this->skinStyles[
'default'] ) ?
847 (array)$this->skinStyles[
'default'] :
857 foreach ( $paths as
$path ) {
861 $this->skinStyles[$skinName] = $styleFiles;
873 return array_merge_recursive(
874 self::collateFilePathListByOption( $this->styles,
'media',
'all' ),
875 self::collateFilePathListByOption(
876 self::tryForKey( $this->skinStyles, $context->
getSkin(),
'default' ),
891 return self::collateFilePathListByOption(
892 self::tryForKey( $this->skinStyles, $skinName ),
905 $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
908 $internalSkinNames = array_keys( $skinFactory->getSkinNames() );
909 $internalSkinNames[] =
'default';
911 foreach ( $internalSkinNames as $internalSkinName ) {
912 $styleFiles = array_merge_recursive(
914 $this->getSkinStyleFiles( $internalSkinName )
927 $collatedStyleFiles = array_merge_recursive(
928 self::collateFilePathListByOption( $this->styles,
'media',
'all' ),
929 $this->getAllSkinStyleFiles()
934 foreach ( $collatedStyleFiles as $media => $styleFiles ) {
935 foreach ( $styleFiles as $styleFile ) {
936 $result[] = $this->getLocalPath( $styleFile );
951 if ( empty( $scripts ) ) {
955 foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
956 $localPath = $this->getLocalPath( $fileName );
957 $contents = $this->getFileContents( $localPath,
'script' );
977 foreach ( $styles as $media => $files ) {
978 $uniqueFiles = array_unique( $files, SORT_REGULAR );
980 foreach ( $uniqueFiles as
$file ) {
981 $styleFiles[] = $this->readStyleFile(
$file, $context );
983 $styles[$media] = implode(
"\n", $styleFiles );
1000 $localPath = $this->getLocalPath(
$path );
1001 $style = $this->getFileContents( $localPath,
'style' );
1002 $styleLang = $this->getStyleSheetLang( $localPath );
1004 return $this->processStyle( $style, $styleLang,
$path, $context );
1024 $localPath = $this->getLocalPath(
$path );
1025 $remotePath = $this->getRemotePath(
$path );
1027 if ( $styleLang ===
'less' ) {
1028 $style = $this->compileLessString( $style, $localPath, $context );
1029 $this->hasGeneratedStyles =
true;
1032 if ( $this->getFlip( $context ) ) {
1033 $style = CSSJanus::transform(
1040 $localDir = dirname( $localPath );
1041 $remoteDir = dirname( $remotePath );
1043 $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
1044 foreach ( $localFileRefs as
$file ) {
1045 if ( file_exists(
$file ) ) {
1046 $this->localFileRefs[] =
$file;
1048 $this->missingLocalFileRefs[] =
$file;
1053 return CSSMin::remap( $style, $localDir, $remoteDir,
true );
1062 return $context->
getDirection() ===
'rtl' && !$this->noflip;
1071 return $this->targets;
1081 $canBeStylesOnly = !(
1084 || $this->debugScripts
1086 || $this->languageScripts
1087 || $this->skinScripts
1088 || $this->dependencies
1090 || $this->skipFunction
1091 || $this->packageFiles
1093 return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
1106 $style = $this->getFileContents( $fileName,
'LESS' );
1107 return $this->compileLessString( $style, $fileName, $context );
1129 $skinName = $context->
getSkin();
1130 $skinImportPaths = ExtensionRegistry::getInstance()->getAttribute(
'SkinLessImportPaths' );
1132 if ( isset( $skinImportPaths[ $skinName ] ) ) {
1133 $importDirs[] = $skinImportPaths[ $skinName ];
1136 $vars = $this->getLessVars( $context );
1142 'importDirs' => $importDirs,
1144 $key =
$cache->makeGlobalKey(
1145 'resourceloader-less',
1147 hash(
'md4', $style ),
1148 hash(
'md4',
serialize( $compilerParams ) )
1153 $data =
$cache->get( $key );
1158 $compiler = $context->
getResourceLoader()->getLessCompiler( $vars, $importDirs );
1160 $css = $compiler->parse( $style, $stylePath )->getCss();
1164 $files = $compiler->AllParsedFiles();
1170 $cache->set( $key, $data, $cache::TTL_DAY );
1174 $this->localFileRefs[] =
$path;
1177 return $data[
'css'];
1188 foreach ( $this->templates as $alias => $templatePath ) {
1190 if ( is_int( $alias ) ) {
1191 $alias = $this->getPath( $templatePath );
1193 $localPath = $this->getLocalPath( $templatePath );
1194 $content = $this->getFileContents( $localPath,
'template' );
1196 $templates[$alias] = $this->stripBom(
$content );
1222 if ( isset( $this->expandedPackageFiles[$hash] ) ) {
1223 return $this->expandedPackageFiles[$hash];
1225 if ( $this->packageFiles ===
null ) {
1228 $expandedFiles = [];
1231 foreach ( $this->packageFiles as $key => $fileInfo ) {
1232 if ( is_string( $fileInfo ) ) {
1233 $fileInfo = [
'name' => $fileInfo,
'file' => $fileInfo ];
1235 if ( !isset( $fileInfo[
'name'] ) ) {
1236 $msg =
"Missing 'name' key in package file info for module '{$this->getName()}'," .
1237 " offset '{$key}'.";
1238 $this->getLogger()->error( $msg );
1239 throw new LogicException( $msg );
1241 $fileName = $fileInfo[
'name'];
1244 $type = $fileInfo[
'type'] ?? self::getPackageFileType( $fileName );
1245 $expanded = [
'type' =>
$type ];
1246 if ( !empty( $fileInfo[
'main'] ) ) {
1247 $mainFile = $fileName;
1248 if (
$type !==
'script' &&
$type !==
'script-vue' ) {
1249 $msg =
"Main file in package must be of type 'script', module " .
1250 "'{$this->getName()}', main file '{$mainFile}' is '{$type}'.";
1251 $this->getLogger()->error( $msg );
1252 throw new LogicException( $msg );
1260 if ( isset( $fileInfo[
'content'] ) ) {
1261 $expanded[
'content'] = $fileInfo[
'content'];
1262 } elseif ( isset( $fileInfo[
'file'] ) ) {
1263 $expanded[
'filePath'] = $fileInfo[
'file'];
1264 } elseif ( isset( $fileInfo[
'callback'] ) ) {
1266 $expanded[
'callbackParam'] = $fileInfo[
'callbackParam'] ??
null;
1268 if ( !is_callable( $fileInfo[
'callback'] ) ) {
1269 $msg =
"Invalid 'callback' for module '{$this->getName()}', file '{$fileName}'.";
1270 $this->getLogger()->error( $msg );
1271 throw new LogicException( $msg );
1273 if ( isset( $fileInfo[
'versionCallback'] ) ) {
1274 if ( !is_callable( $fileInfo[
'versionCallback'] ) ) {
1275 throw new LogicException(
"Invalid 'versionCallback' for "
1276 .
"module '{$this->getName()}', file '{$fileName}'."
1282 $callbackResult = ( $fileInfo[
'versionCallback'] )(
1285 $expanded[
'callbackParam']
1288 $expanded[
'filePath'] = $callbackResult->getPath();
1290 $expanded[
'definitionSummary'] = $callbackResult;
1293 $expanded[
'callback'] = $fileInfo[
'callback'];
1296 $callbackResult = ( $fileInfo[
'callback'] )(
1299 $expanded[
'callbackParam']
1302 $expanded[
'filePath'] = $callbackResult->getPath();
1304 $expanded[
'content'] = $callbackResult;
1307 } elseif ( isset( $fileInfo[
'config'] ) ) {
1308 if (
$type !==
'data' ) {
1309 $msg =
"Key 'config' only valid for data files. "
1310 .
" Module '{$this->getName()}', file '{$fileName}' is '{$type}'.";
1311 $this->getLogger()->error( $msg );
1312 throw new LogicException( $msg );
1314 $expandedConfig = [];
1315 foreach ( $fileInfo[
'config'] as $configKey => $var ) {
1316 $expandedConfig[ is_numeric( $configKey ) ? $var : $configKey ] = $this->getConfig()->get( $var );
1318 $expanded[
'content'] = $expandedConfig;
1319 } elseif ( !empty( $fileInfo[
'main'] ) ) {
1321 $expanded[
'filePath'] = $fileName;
1323 $msg =
"Incomplete definition for module '{$this->getName()}', file '{$fileName}'. "
1324 .
"One of 'file', 'content', 'callback', or 'config' must be set.";
1325 $this->getLogger()->error( $msg );
1326 throw new LogicException( $msg );
1329 $expandedFiles[$fileName] = $expanded;
1332 if ( $expandedFiles && $mainFile ===
null ) {
1334 foreach ( $expandedFiles as
$path =>
$file ) {
1335 if (
$file[
'type'] ===
'script' ||
$file[
'type'] ===
'script-vue' ) {
1343 'main' => $mainFile,
1344 'files' => $expandedFiles
1347 $this->expandedPackageFiles[$hash] = $result;
1358 if ( $this->packageFiles ===
null ) {
1362 if ( isset( $this->fullyExpandedPackageFiles[ $hash ] ) ) {
1363 return $this->fullyExpandedPackageFiles[ $hash ];
1365 $expandedPackageFiles = $this->expandPackageFiles( $context );
1368 foreach ( $expandedPackageFiles[
'files'] as $fileName => &$fileInfo ) {
1373 if ( isset( $fileInfo[
'callback'] ) ) {
1374 $callbackResult = ( $fileInfo[
'callback'] )(
1377 $fileInfo[
'callbackParam']
1381 $fileInfo[
'filePath'] = $callbackResult->getPath();
1383 $fileInfo[
'content'] = $callbackResult;
1385 unset( $fileInfo[
'callback'] );
1392 if ( !isset( $fileInfo[
'content'] ) && isset( $fileInfo[
'filePath'] ) ) {
1393 $localPath = $this->getLocalPath( $fileInfo[
'filePath'] );
1394 $content = $this->getFileContents( $localPath,
'package' );
1395 if ( $fileInfo[
'type'] ===
'data' ) {
1399 unset( $fileInfo[
'filePath'] );
1401 if ( $fileInfo[
'type'] ===
'script-vue' ) {
1403 $parsedComponent = $this->getVueComponentParser()->parse(
1404 $fileInfo[
'content'],
1405 [
'minifyTemplate' => !$context->
getDebug() ]
1407 }
catch ( Exception $e ) {
1408 $msg =
"Error parsing file '$fileName' in module '{$this->getName()}': " .
1410 $this->getLogger()->error( $msg );
1411 throw new RuntimeException( $msg );
1413 $encodedTemplate = json_encode( $parsedComponent[
'template'] );
1417 $encodedTemplate = preg_replace(
'/(?<!\\\\)\\\\n/',
" \\\n", $encodedTemplate );
1419 $encodedTemplate = strtr( $encodedTemplate, [
"\\t" =>
"\t" ] );
1421 $fileInfo[
'content'] = [
1422 'script' => $parsedComponent[
'script'] .
1423 ";\nmodule.exports.template = $encodedTemplate;",
1424 'style' => $parsedComponent[
'style'] ??
'',
1425 'styleLang' => $parsedComponent[
'styleLang'] ??
'css'
1427 $fileInfo[
'type'] =
'script+style';
1431 unset( $fileInfo[
'definitionSummary'] );
1433 unset( $fileInfo[
'callbackParam'] );
1436 $this->fullyExpandedPackageFiles[ $hash ] = $expandedPackageFiles;
1437 return $expandedPackageFiles;
1451 if ( substr_compare(
"\xef\xbb\xbf", $input, 0, 3 ) === 0 ) {
1452 return substr( $input, 3 );
$wgResourceBasePath
The default 'remoteBasePath' value for instances of ResourceLoaderFileModule.
$wgExtensionAssetsPath
The URL path of the extensions directory.
$wgStylePath
The URL path of the skins directory.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
static getFileContentsHash( $filePaths, $algo='md4')
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
Context object that contains information about the state of a specific ResourceLoader web request.
getHash()
All factors that uniquely identify this request, except 'modules'.
Module based on local JavaScript/CSS files.
array $dependencies
List of modules this module depends on.
array $skinStyles
List of paths to CSS files to include when using specific skins.
getScriptFiles(ResourceLoaderContext $context)
Get a list of script file paths for this module, in order of proper execution.
getStyleFiles(ResourceLoaderContext $context)
Get a list of file paths for all styles in this module, in order of proper inclusion.
getTargets()
Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile'].
getStyles(ResourceLoaderContext $context)
Get all styles for a given context.
array $skinScripts
List of JavaScript files to include when using a specific skin.
getFileHashes(ResourceLoaderContext $context)
Helper method for getDefinitionSummary.
string $skipFunction
File name containing the body of the skip function.
static getPackageFileType( $path)
Infer the file type from a package file path.
__construct(array $options=[], $localBasePath=null, $remoteBasePath=null)
Constructs a new module from an options array.
array $languageScripts
List of JavaScript files to include when using a specific language.
setSkinStylesOverride(array $moduleSkinStyles)
Provide overrides for skinStyles to modules that support that.
readScriptFiles(array $scripts)
Get the contents of a list of JavaScript files.
array $scripts
List of paths to JavaScript files to always include.
getFileContents( $localPath, $type)
Helper method for getting a file.
getScriptURLsForDebug(ResourceLoaderContext $context)
getGroup()
Gets the name of the group this module should be loaded in.
getStyleURLsForDebug(ResourceLoaderContext $context)
bool $debugRaw
Link to raw files in debug mode.
array $templates
Saves a list of the templates named by the modules.
getTemplates()
Takes named templates by the module and returns an array mapping.
array $packageFiles
List of packaged files to make available through require()
static tryForKey(array $list, $key, $fallback=null)
Get a list of element that match a key, optionally using a fallback key.
readStyleFile( $path, ResourceLoaderContext $context)
Read and process a style file.
string $localBasePath
Local base path, see __construct()
getMessages()
Gets list of message keys used by this module.
string $group
Name of group to load this module in.
compileLessFile( $fileName, ResourceLoaderContext $context)
expandPackageFiles(ResourceLoaderContext $context)
Internal helper for use by getPackageFiles(), getFileHashes() and getDefinitionSummary().
getDefinitionSummary(ResourceLoaderContext $context)
Get the definition summary for this module.
array $expandedPackageFiles
Expanded versions of $packageFiles, lazy-computed by expandPackageFiles(); keyed by context hash.
array $debugScripts
List of paths to JavaScript files to include in debug mode.
getSkinStyleFiles( $skinName)
Gets a list of file paths for all skin styles in the module used by the skin.
readStyleFiles(array $styles, ResourceLoaderContext $context)
Get the contents of a list of CSS files.
getStyleSheetLang( $path)
Infer the stylesheet language from a stylesheet file path.
requiresES6()
Whether the module requires ES6 support in the client.
VueComponentParser null $vueComponentParser
Lazy-created by getVueComponentParser()
getDependencies(ResourceLoaderContext $context=null)
Gets list of names of modules this module depends on.
enableModuleContentVersion()
Disable module content versioning.
bool $hasGeneratedStyles
Whether getStyleURLsForDebug should return raw file paths, or return load.php urls.
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information.
getPackageFiles(ResourceLoaderContext $context)
Resolves the package files definition and generates the content of each package file.
array $missingLocalFileRefs
Place where readStyleFile() tracks file dependencies for non-existent files.
array $fullyExpandedPackageFiles
Further expanded versions of $expandedPackageFiles, lazy-computed by getPackageFiles(); keyed by cont...
static collateFilePathListByOption(array $list, $option, $default)
Collates file paths by option (where provided).
getScript(ResourceLoaderContext $context)
Gets all scripts for a given context concatenated together.
getType()
Get the module's load type.
stripBom( $input)
Takes an input string and removes the UTF-8 BOM character if present.
getAllSkinStyleFiles()
Gets a list of file paths for all skin style files in the module, for all available skins.
getFlip(ResourceLoaderContext $context)
Get whether CSS for this module should be flipped.
getLanguageScripts( $lang)
Get the set of language scripts for the given language, possibly using a fallback language.
array $styles
List of paths to CSS files to always include.
bool $noflip
Whether CSSJanus flipping should be skipped for this module.
getAllStyleFiles()
Returns all style files and all skin style files used by this module.
array $localFileRefs
Place where readStyleFile() tracks file dependencies.
compileLessString( $style, $stylePath, ResourceLoaderContext $context)
Compile a LESS string into CSS.
array $messages
List of message keys used by this module.
string $remoteBasePath
Remote base path, see __construct()
bool $es6
Whether this module requires the client to support ES6.
processStyle( $style, $styleLang, $path, ResourceLoaderContext $context)
Process a CSS/LESS string.
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path,...
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
getFileDependencies(ResourceLoaderContext $context)
Get the indirect dependencies for this module persuant to the skin/language context.
getMessageBlob(ResourceLoaderContext $context)
Get the hash of the message blob.
getDeprecationInformation(ResourceLoaderContext $context)
Get JS representing deprecation information for the current module if available.
static expandRelativePaths(array $filePaths)
Expand directories relative to $IP.
getLessVars(ResourceLoaderContext $context)
Get module-specific LESS variables, if any.
saveFileDependencies(ResourceLoaderContext $context, array $curFileRefs)
Save the indirect dependencies for this module persuant to the skin/language context.
static getRelativePaths(array $filePaths)
Make file paths relative to MediaWiki directory.
getName()
Get this module's name.
static ensureNewline( $str)
Ensure the string is either empty or ends in a line break.
Parser for Vue single file components (.vue files).
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
if(!isset( $args[0])) $lang