MediaWiki REL1_37
ResourceLoaderFileModule.php
Go to the documentation of this file.
1<?php
25use Wikimedia\Minify\CSSMin;
26
42 protected $localBasePath = '';
43
45 protected $remoteBasePath = '';
46
48 protected $templates = [];
49
57 protected $scripts = [];
58
66 protected $languageScripts = [];
67
75 protected $skinScripts = [];
76
84 protected $debugScripts = [];
85
93 protected $styles = [];
94
102 protected $skinStyles = [];
103
111 protected $packageFiles = null;
112
118
124
132 protected $dependencies = [];
133
137 protected $skipFunction = null;
138
146 protected $messages = [];
147
149 protected $group;
150
152 protected $debugRaw = true;
153
155 protected $targets = [ 'desktop' ];
156
158 protected $noflip = false;
159
161 protected $es6 = false;
162
167 protected $hasGeneratedStyles = false;
168
176 protected $localFileRefs = [];
177
182 protected $missingLocalFileRefs = [];
183
187 protected $vueComponentParser = null;
188
200 public function __construct(
201 array $options = [],
202 $localBasePath = null,
203 $remoteBasePath = null
204 ) {
205 // Flag to decide whether to automagically add the mediawiki.template module
206 $hasTemplates = false;
207 // localBasePath and remoteBasePath both have unbelievably long fallback chains
208 // and need to be handled separately.
209 list( $this->localBasePath, $this->remoteBasePath ) =
211
212 // Extract, validate and normalise remaining options
213 foreach ( $options as $member => $option ) {
214 switch ( $member ) {
215 // Lists of file paths
216 case 'scripts':
217 case 'debugScripts':
218 case 'styles':
219 case 'packageFiles':
220 $this->{$member} = is_array( $option ) ? $option : [ $option ];
221 break;
222 case 'templates':
223 $hasTemplates = true;
224 $this->{$member} = is_array( $option ) ? $option : [ $option ];
225 break;
226 // Collated lists of file paths
227 case 'languageScripts':
228 case 'skinScripts':
229 case 'skinStyles':
230 if ( !is_array( $option ) ) {
231 throw new InvalidArgumentException(
232 "Invalid collated file path list error. " .
233 "'$option' given, array expected."
234 );
235 }
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."
241 );
242 }
243 $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
244 }
245 break;
246 case 'deprecated':
247 $this->deprecated = $option;
248 break;
249 // Lists of strings
250 case 'dependencies':
251 case 'messages':
252 case 'targets':
253 // Normalise
254 $option = array_values( array_unique( (array)$option ) );
255 sort( $option );
256
257 $this->{$member} = $option;
258 break;
259 // Single strings
260 case 'group':
261 case 'skipFunction':
262 $this->{$member} = (string)$option;
263 break;
264 // Single booleans
265 case 'debugRaw':
266 case 'noflip':
267 case 'es6':
268 $this->{$member} = (bool)$option;
269 break;
270 }
271 }
272 if ( isset( $options['scripts'] ) && isset( $options['packageFiles'] ) ) {
273 throw new InvalidArgumentException( "A module may not set both 'scripts' and 'packageFiles'" );
274 }
275 if ( isset( $options['packageFiles'] ) && isset( $options['skinScripts'] ) ) {
276 throw new InvalidArgumentException( "Options 'skinScripts' and 'packageFiles' cannot be used together." );
277 }
278 if ( $hasTemplates ) {
279 $this->dependencies[] = 'mediawiki.template';
280 // Ensure relevant template compiler module gets loaded
281 foreach ( $this->templates as $alias => $templatePath ) {
282 if ( is_int( $alias ) ) {
283 $alias = $this->getPath( $templatePath );
284 }
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;
290 }
291 }
292 }
293 }
294
306 public static function extractBasePaths(
307 array $options = [],
308 $localBasePath = null,
309 $remoteBasePath = null
310 ) {
311 global $IP, $wgResourceBasePath;
312
313 // The different ways these checks are done, and their ordering, look very silly,
314 // but were preserved for backwards-compatibility just in case. Tread lightly.
315
316 if ( $localBasePath === null ) {
318 }
319 if ( $remoteBasePath === null ) {
321 }
322
323 if ( isset( $options['remoteExtPath'] ) ) {
325 $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
326 }
327
328 if ( isset( $options['remoteSkinPath'] ) ) {
329 global $wgStylePath;
330 $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
331 }
332
333 if ( array_key_exists( 'localBasePath', $options ) ) {
334 $localBasePath = (string)$options['localBasePath'];
335 }
336
337 if ( array_key_exists( 'remoteBasePath', $options ) ) {
338 $remoteBasePath = (string)$options['remoteBasePath'];
339 }
340
341 if ( $remoteBasePath === '' ) {
342 // If MediaWiki is installed at the document root (not recommended),
343 // then wgScriptPath is set to the empty string by the installer to
344 // ensure safe concatenating of file paths (avoid "/" + "/foo" being "//foo").
345 // However, this also means the path itself can be an invalid URI path,
346 // as those must start with a slash. Within ResourceLoader, we will not
347 // do such primitive/unsafe slash concatenation and use URI resolution
348 // instead, so beyond this point, to avoid fatal errors in CSSMin::resolveUrl(),
349 // do a best-effort support for docroot installs by casting this to a slash.
350 $remoteBasePath = '/';
351 }
352
354 }
355
362 public function getScript( ResourceLoaderContext $context ) {
363 $deprecationScript = $this->getDeprecationInformation( $context );
364 if ( $this->packageFiles !== null ) {
365 $packageFiles = $this->getPackageFiles( $context );
366 foreach ( $packageFiles['files'] as &$file ) {
367 if ( $file['type'] === 'script+style' ) {
368 $file['content'] = $file['content']['script'];
369 $file['type'] = 'script';
370 }
371 }
372 if ( $deprecationScript ) {
373 $mainFile =& $packageFiles['files'][$packageFiles['main']];
374 $mainFile['content'] = $deprecationScript . $mainFile['content'];
375 }
376 return $packageFiles;
377 }
378
379 $files = $this->getScriptFiles( $context );
380 return $deprecationScript . $this->readScriptFiles( $files );
381 }
382
387 public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
388 $urls = [];
389 foreach ( $this->getScriptFiles( $context ) as $file ) {
390 $urls[] = OutputPage::transformResourcePath(
391 $this->getConfig(),
392 $this->getRemotePath( $file )
393 );
394 }
395 return $urls;
396 }
397
401 public function supportsURLLoading() {
402 // If package files are involved, don't support URL loading, because that breaks
403 // scoped require() functions
404 return $this->debugRaw && !$this->packageFiles;
405 }
406
413 public function getStyles( ResourceLoaderContext $context ) {
414 $styles = $this->readStyleFiles(
415 $this->getStyleFiles( $context ),
416 $context
417 );
418
419 if ( $this->packageFiles !== null ) {
420 $packageFiles = $this->getPackageFiles( $context );
421 foreach ( $packageFiles['files'] as $fileName => $file ) {
422 if ( $file['type'] === 'script+style' ) {
423 $style = $this->processStyle(
424 $file['content']['style'],
425 $file['content']['styleLang'],
426 $fileName,
427 $context
428 );
429 $styles['all'] = ( $styles['all'] ?? '' ) . "\n" . $style;
430 }
431 }
432 }
433
434 // Track indirect file dependencies so that ResourceLoaderStartUpModule can check for
435 // on-disk file changes to any of this files without having to recompute the file list
436 $this->saveFileDependencies( $context, $this->localFileRefs );
437
438 return $styles;
439 }
440
445 public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
446 if ( $this->hasGeneratedStyles ) {
447 // Do the default behaviour of returning a url back to load.php
448 // but with only=styles.
449 return parent::getStyleURLsForDebug( $context );
450 }
451 // Our module consists entirely of real css files,
452 // in debug mode we can load those directly.
453 $urls = [];
454 foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
455 $urls[$mediaType] = [];
456 foreach ( $list as $file ) {
457 $urls[$mediaType][] = OutputPage::transformResourcePath(
458 $this->getConfig(),
459 $this->getRemotePath( $file )
460 );
461 }
462 }
463 return $urls;
464 }
465
471 public function getMessages() {
472 return $this->messages;
473 }
474
480 public function getGroup() {
481 return $this->group;
482 }
483
489 public function getDependencies( ResourceLoaderContext $context = null ) {
490 return $this->dependencies;
491 }
492
501 private function getFileContents( $localPath, $type ) {
502 if ( !is_file( $localPath ) ) {
503 throw new RuntimeException(
504 __METHOD__ . ": $type file not found, or is not a file: \"$localPath\""
505 );
506 }
507 return $this->stripBom( file_get_contents( $localPath ) );
508 }
509
514 public function getSkipFunction() {
515 if ( !$this->skipFunction ) {
516 return null;
517 }
518 $localPath = $this->getLocalPath( $this->skipFunction );
519 return $this->getFileContents( $localPath, 'skip function' );
520 }
521
522 public function requiresES6() {
523 return $this->es6;
524 }
525
534 public function enableModuleContentVersion() {
535 return false;
536 }
537
544 private function getFileHashes( ResourceLoaderContext $context ) {
545 $files = [];
546
547 $styleFiles = $this->getStyleFiles( $context );
548 foreach ( $styleFiles as $paths ) {
549 $files = array_merge( $files, $paths );
550 }
551
552 // Extract file paths for package files
553 // Optimisation: Use foreach() and isset() instead of array_map/array_filter.
554 // This is a hot code path, called by StartupModule for thousands of modules.
555 $expandedPackageFiles = $this->expandPackageFiles( $context );
556 $packageFiles = [];
557 if ( $expandedPackageFiles ) {
558 foreach ( $expandedPackageFiles['files'] as $fileInfo ) {
559 if ( isset( $fileInfo['filePath'] ) ) {
560 $packageFiles[] = $fileInfo['filePath'];
561 }
562 }
563 }
564
565 // Merge all the file paths we were able discover directly from the module definition.
566 // This is the master list of direct-dependent files for this module.
567 $files = array_merge(
568 $files,
570 $this->scripts,
571 $this->templates,
572 $context->getDebug() ? $this->debugScripts : [],
573 $this->getLanguageScripts( $context->getLanguage() ),
574 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
575 );
576 if ( $this->skipFunction ) {
577 $files[] = $this->skipFunction;
578 }
579
580 // Expand these local paths into absolute file paths
581 $files = array_map( [ $this, 'getLocalPath' ], $files );
582
583 // Add any lazily discovered file dependencies from previous module builds.
584 // These are added last because they are already absolute file paths.
585 $files = array_merge( $files, $this->getFileDependencies( $context ) );
586
587 // Filter out any duplicates. Typically introduced by getFileDependencies() which
588 // may lazily re-discover a master file.
589 $files = array_unique( $files );
590
591 // Don't return array keys or any other form of file path here, only the hashes.
592 // Including file paths would needlessly cause global cache invalidation when files
593 // move on disk or if e.g. the MediaWiki directory name changes.
594 // Anything where order is significant is already detected by the definition summary.
596 }
597
604 public function getDefinitionSummary( ResourceLoaderContext $context ) {
605 $summary = parent::getDefinitionSummary( $context );
606
607 $options = [];
608 foreach ( [
609 // The following properties are omitted because they don't affect the module reponse:
610 // - localBasePath (Per T104950; Changes when absolute directory name changes. If
611 // this affects 'scripts' and other file paths, getFileHashes accounts for that.)
612 // - remoteBasePath (Per T104950)
613 // - dependencies (provided via startup module)
614 // - targets
615 // - group (provided via startup module)
616 'scripts',
617 'debugScripts',
618 'styles',
619 'languageScripts',
620 'skinScripts',
621 'skinStyles',
622 'messages',
623 'templates',
624 'skipFunction',
625 'debugRaw',
626 ] as $member ) {
627 $options[$member] = $this->{$member};
628 }
629
630 $packageFiles = $this->expandPackageFiles( $context );
631 if ( $packageFiles ) {
632 // Extract the minimum needed:
633 // - The 'main' pointer (included as-is).
634 // - The 'files' array, simplified to only which files exist (the keys of
635 // this array), and something that represents their non-file content.
636 // For packaged files that reflect files directly from disk, the
637 // 'getFileHashes' method tracks their content already.
638 // It is important that the keys of the $packageFiles['files'] array
639 // are preserved, as they do affect the module output.
640 $packageFiles['files'] = array_map( static function ( $fileInfo ) {
641 return $fileInfo['definitionSummary'] ?? ( $fileInfo['content'] ?? null );
642 }, $packageFiles['files'] );
643 }
644
645 $summary[] = [
646 'options' => $options,
647 'packageFiles' => $packageFiles,
648 'fileHashes' => $this->getFileHashes( $context ),
649 'messageBlob' => $this->getMessageBlob( $context ),
650 ];
651
652 $lessVars = $this->getLessVars( $context );
653 if ( $lessVars ) {
654 $summary[] = [ 'lessVars' => $lessVars ];
655 }
656
657 return $summary;
658 }
659
663 protected function getVueComponentParser() {
664 if ( $this->vueComponentParser === null ) {
665 $this->vueComponentParser = new VueComponentParser;
666 }
668 }
669
674 protected function getPath( $path ) {
675 if ( $path instanceof ResourceLoaderFilePath ) {
676 return $path->getPath();
677 }
678
679 return $path;
680 }
681
686 protected function getLocalPath( $path ) {
687 if ( $path instanceof ResourceLoaderFilePath ) {
688 return $path->getLocalPath();
689 }
690
691 return "{$this->localBasePath}/$path";
692 }
693
698 protected function getRemotePath( $path ) {
699 if ( $path instanceof ResourceLoaderFilePath ) {
700 return $path->getRemotePath();
701 }
702
703 if ( $this->remoteBasePath === '/' ) {
704 return "/$path";
705 } else {
706 return "{$this->remoteBasePath}/$path";
707 }
708 }
709
717 public function getStyleSheetLang( $path ) {
718 return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
719 }
720
726 public static function getPackageFileType( $path ) {
727 if ( preg_match( '/\.json$/i', $path ) ) {
728 return 'data';
729 }
730 if ( preg_match( '/\.vue$/i', $path ) ) {
731 return 'script-vue';
732 }
733 return 'script';
734 }
735
745 protected static function collateFilePathListByOption( array $list, $option, $default ) {
746 $collatedFiles = [];
747 foreach ( $list as $key => $value ) {
748 if ( is_int( $key ) ) {
749 // File name as the value
750 if ( !isset( $collatedFiles[$default] ) ) {
751 $collatedFiles[$default] = [];
752 }
753 $collatedFiles[$default][] = $value;
754 } elseif ( is_array( $value ) ) {
755 // File name as the key, options array as the value
756 $optionValue = $value[$option] ?? $default;
757 if ( !isset( $collatedFiles[$optionValue] ) ) {
758 $collatedFiles[$optionValue] = [];
759 }
760 $collatedFiles[$optionValue][] = $key;
761 }
762 }
763 return $collatedFiles;
764 }
765
775 protected static function tryForKey( array $list, $key, $fallback = null ) {
776 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
777 return $list[$key];
778 } elseif ( is_string( $fallback )
779 && isset( $list[$fallback] )
780 && is_array( $list[$fallback] )
781 ) {
782 return $list[$fallback];
783 }
784 return [];
785 }
786
793 private function getScriptFiles( ResourceLoaderContext $context ) {
794 $files = array_merge(
795 $this->scripts,
796 $this->getLanguageScripts( $context->getLanguage() ),
797 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
798 );
799 if ( $context->getDebug() ) {
800 $files = array_merge( $files, $this->debugScripts );
801 }
802
803 return array_unique( $files, SORT_REGULAR );
804 }
805
813 private function getLanguageScripts( $lang ) {
814 $scripts = self::tryForKey( $this->languageScripts, $lang );
815 if ( $scripts ) {
816 return $scripts;
817 }
818 $fallbacks = MediaWikiServices::getInstance()->getLanguageFallback()
819 ->getAll( $lang, LanguageFallback::MESSAGES );
820 foreach ( $fallbacks as $lang ) {
821 $scripts = self::tryForKey( $this->languageScripts, $lang );
822 if ( $scripts ) {
823 return $scripts;
824 }
825 }
826
827 return [];
828 }
829
830 public function setSkinStylesOverride( array $moduleSkinStyles ): void {
831 $moduleName = $this->getName();
832 foreach ( $moduleSkinStyles as $skinName => $overrides ) {
833 // If a module provides overrides for a skin, and that skin also provides overrides
834 // for the same module, then the module has precedence.
835 if ( isset( $this->skinStyles[$skinName] ) ) {
836 continue;
837 }
838
839 // If $moduleName in ResourceModuleSkinStyles is preceded with a '+', the defined style
840 // files will be added to 'default' skinStyles, otherwise 'default' will be ignored.
841 if ( isset( $overrides[$moduleName] ) ) {
842 $paths = (array)$overrides[$moduleName];
843 $styleFiles = [];
844 } elseif ( isset( $overrides['+' . $moduleName] ) ) {
845 $paths = (array)$overrides['+' . $moduleName];
846 $styleFiles = isset( $this->skinStyles['default'] ) ?
847 (array)$this->skinStyles['default'] :
848 [];
849 } else {
850 continue;
851 }
852
853 // Add new file paths, remapping them to refer to our directories and not use settings
854 // from the module we're modifying, which come from the base definition.
856
857 foreach ( $paths as $path ) {
859 }
860
861 $this->skinStyles[$skinName] = $styleFiles;
862 }
863 }
864
872 public function getStyleFiles( ResourceLoaderContext $context ) {
873 return array_merge_recursive(
874 self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
875 self::collateFilePathListByOption(
876 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
877 'media',
878 'all'
879 )
880 );
881 }
882
890 protected function getSkinStyleFiles( $skinName ) {
891 return self::collateFilePathListByOption(
892 self::tryForKey( $this->skinStyles, $skinName ),
893 'media',
894 'all'
895 );
896 }
897
904 protected function getAllSkinStyleFiles() {
905 $skinFactory = MediaWikiServices::getInstance()->getSkinFactory();
906 $styleFiles = [];
907
908 $internalSkinNames = array_keys( $skinFactory->getSkinNames() );
909 $internalSkinNames[] = 'default';
910
911 foreach ( $internalSkinNames as $internalSkinName ) {
912 $styleFiles = array_merge_recursive(
913 $styleFiles,
914 $this->getSkinStyleFiles( $internalSkinName )
915 );
916 }
917
918 return $styleFiles;
919 }
920
926 public function getAllStyleFiles() {
927 $collatedStyleFiles = array_merge_recursive(
928 self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
929 $this->getAllSkinStyleFiles()
930 );
931
932 $result = [];
933
934 foreach ( $collatedStyleFiles as $media => $styleFiles ) {
935 foreach ( $styleFiles as $styleFile ) {
936 $result[] = $this->getLocalPath( $styleFile );
937 }
938 }
939
940 return $result;
941 }
942
950 private function readScriptFiles( array $scripts ) {
951 if ( empty( $scripts ) ) {
952 return '';
953 }
954 $js = '';
955 foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
956 $localPath = $this->getLocalPath( $fileName );
957 $contents = $this->getFileContents( $localPath, 'script' );
958 $js .= ResourceLoader::ensureNewline( $contents );
959 }
960 return $js;
961 }
962
973 public function readStyleFiles( array $styles, ResourceLoaderContext $context ) {
974 if ( !$styles ) {
975 return [];
976 }
977 foreach ( $styles as $media => $files ) {
978 $uniqueFiles = array_unique( $files, SORT_REGULAR );
979 $styleFiles = [];
980 foreach ( $uniqueFiles as $file ) {
981 $styleFiles[] = $this->readStyleFile( $file, $context );
982 }
983 $styles[$media] = implode( "\n", $styleFiles );
984 }
985 return $styles;
986 }
987
999 protected function readStyleFile( $path, ResourceLoaderContext $context ) {
1000 $localPath = $this->getLocalPath( $path );
1001 $style = $this->getFileContents( $localPath, 'style' );
1002 $styleLang = $this->getStyleSheetLang( $localPath );
1003
1004 return $this->processStyle( $style, $styleLang, $path, $context );
1005 }
1006
1023 protected function processStyle( $style, $styleLang, $path, ResourceLoaderContext $context ) {
1024 $localPath = $this->getLocalPath( $path );
1025 $remotePath = $this->getRemotePath( $path );
1026
1027 if ( $styleLang === 'less' ) {
1028 $style = $this->compileLessString( $style, $localPath, $context );
1029 $this->hasGeneratedStyles = true;
1030 }
1031
1032 if ( $this->getFlip( $context ) ) {
1033 $style = CSSJanus::transform(
1034 $style,
1035 /* $swapLtrRtlInURL = */ true,
1036 /* $swapLeftRightInURL = */ false
1037 );
1038 }
1039
1040 $localDir = dirname( $localPath );
1041 $remoteDir = dirname( $remotePath );
1042 // Get and register local file references
1043 $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
1044 foreach ( $localFileRefs as $file ) {
1045 if ( file_exists( $file ) ) {
1046 $this->localFileRefs[] = $file;
1047 } else {
1048 $this->missingLocalFileRefs[] = $file;
1049 }
1050 }
1051 // Don't cache this call. remap() ensures data URIs embeds are up to date,
1052 // and urls contain correct content hashes in their query string. (T128668)
1053 return CSSMin::remap( $style, $localDir, $remoteDir, true );
1054 }
1055
1061 public function getFlip( ResourceLoaderContext $context ) {
1062 return $context->getDirection() === 'rtl' && !$this->noflip;
1063 }
1064
1070 public function getTargets() {
1071 return $this->targets;
1072 }
1073
1080 public function getType() {
1081 $canBeStylesOnly = !(
1082 // All options except 'styles', 'skinStyles' and 'debugRaw'
1083 $this->scripts
1084 || $this->debugScripts
1085 || $this->templates
1086 || $this->languageScripts
1087 || $this->skinScripts
1088 || $this->dependencies
1089 || $this->messages
1090 || $this->skipFunction
1091 || $this->packageFiles
1092 );
1093 return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
1094 }
1095
1103 protected function compileLessFile( $fileName, ResourceLoaderContext $context ) {
1104 wfDeprecated( __METHOD__, '1.35' );
1105
1106 $style = $this->getFileContents( $fileName, 'LESS' );
1107 return $this->compileLessString( $style, $fileName, $context );
1108 }
1109
1122 protected function compileLessString( $style, $stylePath, ResourceLoaderContext $context ) {
1123 static $cache;
1124 // @TODO: dependency injection
1125 if ( !$cache ) {
1126 $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
1127 }
1128
1129 $skinName = $context->getSkin();
1130 $skinImportPaths = ExtensionRegistry::getInstance()->getAttribute( 'SkinLessImportPaths' );
1131 $importDirs = [];
1132 if ( isset( $skinImportPaths[ $skinName ] ) ) {
1133 $importDirs[] = $skinImportPaths[ $skinName ];
1134 }
1135
1136 $vars = $this->getLessVars( $context );
1137 // Construct a cache key from a hash of the LESS source, and a hash digest
1138 // of the LESS variables used for compilation.
1139 ksort( $vars );
1140 $compilerParams = [
1141 'vars' => $vars,
1142 'importDirs' => $importDirs,
1143 ];
1144 $key = $cache->makeGlobalKey(
1145 'resourceloader-less',
1146 'v1',
1147 hash( 'md4', $style ),
1148 hash( 'md4', serialize( $compilerParams ) )
1149 );
1150
1151 // If we got a cached value, we have to validate it by getting a checksum of all the
1152 // files that were loaded by the parser and ensuring it matches the cached entry's.
1153 $data = $cache->get( $key );
1154 if (
1155 !$data ||
1156 $data['hash'] !== FileContentsHasher::getFileContentsHash( $data['files'] )
1157 ) {
1158 $compiler = $context->getResourceLoader()->getLessCompiler( $vars, $importDirs );
1159
1160 $css = $compiler->parse( $style, $stylePath )->getCss();
1161 // T253055: store the implicit dependency paths in a form relative to any install
1162 // path so that multiple version of the application can share the cache for identical
1163 // less stylesheets. This also avoids churn during application updates.
1164 $files = $compiler->AllParsedFiles();
1165 $data = [
1166 'css' => $css,
1167 'files' => ResourceLoaderModule::getRelativePaths( $files ),
1168 'hash' => FileContentsHasher::getFileContentsHash( $files )
1169 ];
1170 $cache->set( $key, $data, $cache::TTL_DAY );
1171 }
1172
1173 foreach ( ResourceLoaderModule::expandRelativePaths( $data['files'] ) as $path ) {
1174 $this->localFileRefs[] = $path;
1175 }
1176
1177 return $data['css'];
1178 }
1179
1185 public function getTemplates() {
1186 $templates = [];
1187
1188 foreach ( $this->templates as $alias => $templatePath ) {
1189 // Alias is optional
1190 if ( is_int( $alias ) ) {
1191 $alias = $this->getPath( $templatePath );
1192 }
1193 $localPath = $this->getLocalPath( $templatePath );
1194 $content = $this->getFileContents( $localPath, 'template' );
1195
1196 $templates[$alias] = $this->stripBom( $content );
1197 }
1198 return $templates;
1199 }
1200
1220 private function expandPackageFiles( ResourceLoaderContext $context ) {
1221 $hash = $context->getHash();
1222 if ( isset( $this->expandedPackageFiles[$hash] ) ) {
1223 return $this->expandedPackageFiles[$hash];
1224 }
1225 if ( $this->packageFiles === null ) {
1226 return null;
1227 }
1228 $expandedFiles = [];
1229 $mainFile = null;
1230
1231 foreach ( $this->packageFiles as $key => $fileInfo ) {
1232 if ( is_string( $fileInfo ) ) {
1233 $fileInfo = [ 'name' => $fileInfo, 'file' => $fileInfo ];
1234 }
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 );
1240 }
1241 $fileName = $fileInfo['name'];
1242
1243 // Infer type from alias if needed
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 );
1253 }
1254 }
1255
1256 // Perform expansions (except 'file' and 'callback'), creating one of these keys:
1257 // - 'content': literal value.
1258 // - 'filePath': content to be read from a file.
1259 // - 'callback': content computed by a callable.
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'] ) ) {
1265 // If no extra parameter for the callback is given, use null.
1266 $expanded['callbackParam'] = $fileInfo['callbackParam'] ?? null;
1267
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 );
1272 }
1273 if ( isset( $fileInfo['versionCallback'] ) ) {
1274 if ( !is_callable( $fileInfo['versionCallback'] ) ) {
1275 throw new LogicException( "Invalid 'versionCallback' for "
1276 . "module '{$this->getName()}', file '{$fileName}'."
1277 );
1278 }
1279
1280 // Execute the versionCallback with the same arguments that
1281 // would be given to the callback
1282 $callbackResult = ( $fileInfo['versionCallback'] )(
1283 $context,
1284 $this->getConfig(),
1285 $expanded['callbackParam']
1286 );
1287 if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1288 $expanded['filePath'] = $callbackResult->getPath();
1289 } else {
1290 $expanded['definitionSummary'] = $callbackResult;
1291 }
1292 // Don't invoke 'callback' here as it may be expensive (T223260).
1293 $expanded['callback'] = $fileInfo['callback'];
1294 } else {
1295 // Else go ahead invoke callback with its arguments.
1296 $callbackResult = ( $fileInfo['callback'] )(
1297 $context,
1298 $this->getConfig(),
1299 $expanded['callbackParam']
1300 );
1301 if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1302 $expanded['filePath'] = $callbackResult->getPath();
1303 } else {
1304 $expanded['content'] = $callbackResult;
1305 }
1306 }
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 );
1313 }
1314 $expandedConfig = [];
1315 foreach ( $fileInfo['config'] as $configKey => $var ) {
1316 $expandedConfig[ is_numeric( $configKey ) ? $var : $configKey ] = $this->getConfig()->get( $var );
1317 }
1318 $expanded['content'] = $expandedConfig;
1319 } elseif ( !empty( $fileInfo['main'] ) ) {
1320 // [ 'name' => 'foo.js', 'main' => true ] is shorthand
1321 $expanded['filePath'] = $fileName;
1322 } else {
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 );
1327 }
1328
1329 $expandedFiles[$fileName] = $expanded;
1330 }
1331
1332 if ( $expandedFiles && $mainFile === null ) {
1333 // The first package file that is a script is the main file
1334 foreach ( $expandedFiles as $path => $file ) {
1335 if ( $file['type'] === 'script' || $file['type'] === 'script-vue' ) {
1336 $mainFile = $path;
1337 break;
1338 }
1339 }
1340 }
1341
1342 $result = [
1343 'main' => $mainFile,
1344 'files' => $expandedFiles
1345 ];
1346
1347 $this->expandedPackageFiles[$hash] = $result;
1348 return $result;
1349 }
1350
1357 public function getPackageFiles( ResourceLoaderContext $context ) {
1358 if ( $this->packageFiles === null ) {
1359 return null;
1360 }
1361 $hash = $context->getHash();
1362 if ( isset( $this->fullyExpandedPackageFiles[ $hash ] ) ) {
1363 return $this->fullyExpandedPackageFiles[ $hash ];
1364 }
1365 $expandedPackageFiles = $this->expandPackageFiles( $context );
1366
1367 // Expand file contents
1368 foreach ( $expandedPackageFiles['files'] as $fileName => &$fileInfo ) {
1369 // Turn any 'filePath' or 'callback' key into actual 'content',
1370 // and remove the key after that. The callback could return a
1371 // ResourceLoaderFilePath object; if that happens, fall through
1372 // to the 'filePath' handling.
1373 if ( isset( $fileInfo['callback'] ) ) {
1374 $callbackResult = ( $fileInfo['callback'] )(
1375 $context,
1376 $this->getConfig(),
1377 $fileInfo['callbackParam']
1378 );
1379 if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1380 // Fall through to the filePath handling code below
1381 $fileInfo['filePath'] = $callbackResult->getPath();
1382 } else {
1383 $fileInfo['content'] = $callbackResult;
1384 }
1385 unset( $fileInfo['callback'] );
1386 }
1387 // Only interpret 'filePath' if 'content' hasn't been set already.
1388 // This can happen if 'versionCallback' provided 'filePath',
1389 // while 'callback' provides 'content'. In that case both are set
1390 // at this point. The 'filePath' from 'versionCallback' in that case is
1391 // only to inform getDefinitionSummary().
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' ) {
1396 $content = json_decode( $content );
1397 }
1398 $fileInfo['content'] = $content;
1399 unset( $fileInfo['filePath'] );
1400 }
1401 if ( $fileInfo['type'] === 'script-vue' ) {
1402 try {
1403 $parsedComponent = $this->getVueComponentParser()->parse(
1404 $fileInfo['content'],
1405 [ 'minifyTemplate' => !$context->getDebug() ]
1406 );
1407 } catch ( Exception $e ) {
1408 $msg = "Error parsing file '$fileName' in module '{$this->getName()}': " .
1409 $e->getMessage();
1410 $this->getLogger()->error( $msg );
1411 throw new RuntimeException( $msg );
1412 }
1413 $encodedTemplate = json_encode( $parsedComponent['template'] );
1414 if ( $context->getDebug() ) {
1415 // Replace \n (backslash-n) with space + backslash-newline in debug mode
1416 // We only replace \n if not preceded by a backslash, to avoid breaking '\\n'
1417 $encodedTemplate = preg_replace( '/(?<!\\\\)\\\\n/', " \\\n", $encodedTemplate );
1418 // Expand \t to real tabs in debug mode
1419 $encodedTemplate = strtr( $encodedTemplate, [ "\\t" => "\t" ] );
1420 }
1421 $fileInfo['content'] = [
1422 'script' => $parsedComponent['script'] .
1423 ";\nmodule.exports.template = $encodedTemplate;",
1424 'style' => $parsedComponent['style'] ?? '',
1425 'styleLang' => $parsedComponent['styleLang'] ?? 'css'
1426 ];
1427 $fileInfo['type'] = 'script+style';
1428 }
1429
1430 // Not needed for client response, exists for use by getDefinitionSummary().
1431 unset( $fileInfo['definitionSummary'] );
1432 // Not needed for client response, used by callbacks only.
1433 unset( $fileInfo['callbackParam'] );
1434 }
1435
1436 $this->fullyExpandedPackageFiles[ $hash ] = $expandedPackageFiles;
1437 return $expandedPackageFiles;
1438 }
1439
1450 protected function stripBom( $input ) {
1451 if ( substr_compare( "\xef\xbb\xbf", $input, 0, 3 ) === 0 ) {
1452 return substr( $input, 3 );
1453 }
1454 return $input;
1455 }
1456}
serialize()
$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.
const CACHE_ANYTHING
Definition Defines.php:85
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
$fallback
$IP
Definition WebStart.php:49
static getFileContentsHash( $filePaths, $algo='md4')
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
MediaWikiServices is the service locator for the application scope of MediaWiki.
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).
$cache
Definition mcc.php:33
$content
Definition router.php:76
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!isset( $args[0])) $lang