MediaWiki REL1_35
ResourceLoaderFileModule.php
Go to the documentation of this file.
1<?php
25
41 protected $localBasePath = '';
42
44 protected $remoteBasePath = '';
45
47 protected $templates = [];
48
56 protected $scripts = [];
57
65 protected $languageScripts = [];
66
74 protected $skinScripts = [];
75
83 protected $debugScripts = [];
84
92 protected $styles = [];
93
101 protected $skinStyles = [];
102
110 protected $packageFiles = null;
111
117
123
131 protected $dependencies = [];
132
136 protected $skipFunction = null;
137
145 protected $messages = [];
146
148 protected $group;
149
151 protected $debugRaw = true;
152
153 protected $targets = [ 'desktop' ];
154
156 protected $noflip = false;
157
162 protected $hasGeneratedStyles = false;
163
171 protected $localFileRefs = [];
172
177 protected $missingLocalFileRefs = [];
178
182 protected $vueComponentParser = null;
183
195 public function __construct(
196 array $options = [],
197 $localBasePath = null,
198 $remoteBasePath = null
199 ) {
200 // Flag to decide whether to automagically add the mediawiki.template module
201 $hasTemplates = false;
202 // localBasePath and remoteBasePath both have unbelievably long fallback chains
203 // and need to be handled separately.
204 list( $this->localBasePath, $this->remoteBasePath ) =
206
207 // Extract, validate and normalise remaining options
208 foreach ( $options as $member => $option ) {
209 switch ( $member ) {
210 // Lists of file paths
211 case 'scripts':
212 case 'debugScripts':
213 case 'styles':
214 case 'packageFiles':
215 $this->{$member} = is_array( $option ) ? $option : [ $option ];
216 break;
217 case 'templates':
218 $hasTemplates = true;
219 $this->{$member} = is_array( $option ) ? $option : [ $option ];
220 break;
221 // Collated lists of file paths
222 case 'languageScripts':
223 case 'skinScripts':
224 case 'skinStyles':
225 if ( !is_array( $option ) ) {
226 throw new InvalidArgumentException(
227 "Invalid collated file path list error. " .
228 "'$option' given, array expected."
229 );
230 }
231 foreach ( $option as $key => $value ) {
232 if ( !is_string( $key ) ) {
233 throw new InvalidArgumentException(
234 "Invalid collated file path list key error. " .
235 "'$key' given, string expected."
236 );
237 }
238 $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
239 }
240 break;
241 case 'deprecated':
242 $this->deprecated = $option;
243 break;
244 // Lists of strings
245 case 'dependencies':
246 case 'messages':
247 case 'targets':
248 // Normalise
249 $option = array_values( array_unique( (array)$option ) );
250 sort( $option );
251
252 $this->{$member} = $option;
253 break;
254 // Single strings
255 case 'group':
256 case 'skipFunction':
257 $this->{$member} = (string)$option;
258 break;
259 // Single booleans
260 case 'debugRaw':
261 case 'noflip':
262 $this->{$member} = (bool)$option;
263 break;
264 }
265 }
266 if ( isset( $options['scripts'] ) && isset( $options['packageFiles'] ) ) {
267 throw new InvalidArgumentException( "A module may not set both 'scripts' and 'packageFiles'" );
268 }
269 if ( isset( $options['packageFiles'] ) && isset( $options['skinScripts'] ) ) {
270 throw new InvalidArgumentException( "Options 'skinScripts' and 'packageFiles' cannot be used together." );
271 }
272 if ( $hasTemplates ) {
273 $this->dependencies[] = 'mediawiki.template';
274 // Ensure relevant template compiler module gets loaded
275 foreach ( $this->templates as $alias => $templatePath ) {
276 if ( is_int( $alias ) ) {
277 $alias = $this->getPath( $templatePath );
278 }
279 $suffix = explode( '.', $alias );
280 $suffix = end( $suffix );
281 $compilerModule = 'mediawiki.template.' . $suffix;
282 if ( $suffix !== 'html' && !in_array( $compilerModule, $this->dependencies ) ) {
283 $this->dependencies[] = $compilerModule;
284 }
285 }
286 }
287 }
288
300 public static function extractBasePaths(
301 array $options = [],
302 $localBasePath = null,
303 $remoteBasePath = null
304 ) {
305 global $IP, $wgResourceBasePath;
306
307 // The different ways these checks are done, and their ordering, look very silly,
308 // but were preserved for backwards-compatibility just in case. Tread lightly.
309
310 if ( $localBasePath === null ) {
312 }
313 if ( $remoteBasePath === null ) {
315 }
316
317 if ( isset( $options['remoteExtPath'] ) ) {
319 $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
320 }
321
322 if ( isset( $options['remoteSkinPath'] ) ) {
323 global $wgStylePath;
324 $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
325 }
326
327 if ( array_key_exists( 'localBasePath', $options ) ) {
328 $localBasePath = (string)$options['localBasePath'];
329 }
330
331 if ( array_key_exists( 'remoteBasePath', $options ) ) {
332 $remoteBasePath = (string)$options['remoteBasePath'];
333 }
334
336 }
337
344 public function getScript( ResourceLoaderContext $context ) {
345 $deprecationScript = $this->getDeprecationInformation( $context );
346 if ( $this->packageFiles !== null ) {
347 $packageFiles = $this->getPackageFiles( $context );
348 foreach ( $packageFiles['files'] as &$file ) {
349 if ( $file['type'] === 'script+style' ) {
350 $file['content'] = $file['content']['script'];
351 $file['type'] = 'script';
352 }
353 }
354 if ( $deprecationScript ) {
355 $mainFile =& $packageFiles['files'][$packageFiles['main']];
356 $mainFile['content'] = $deprecationScript . $mainFile['content'];
357 }
358 return $packageFiles;
359 }
360
361 $files = $this->getScriptFiles( $context );
362 return $deprecationScript . $this->readScriptFiles( $files );
363 }
364
369 public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
370 $urls = [];
371 foreach ( $this->getScriptFiles( $context ) as $file ) {
372 $urls[] = OutputPage::transformResourcePath(
373 $this->getConfig(),
374 $this->getRemotePath( $file )
375 );
376 }
377 return $urls;
378 }
379
383 public function supportsURLLoading() {
384 // If package files are involved, don't support URL loading, because that breaks
385 // scoped require() functions
386 return $this->debugRaw && !$this->packageFiles;
387 }
388
395 public function getStyles( ResourceLoaderContext $context ) {
396 $styles = $this->readStyleFiles(
397 $this->getStyleFiles( $context ),
398 $context
399 );
400
401 if ( $this->packageFiles !== null ) {
402 $packageFiles = $this->getPackageFiles( $context );
403 foreach ( $packageFiles['files'] as $fileName => $file ) {
404 if ( $file['type'] === 'script+style' ) {
405 $style = $this->processStyle(
406 $file['content']['style'],
407 $file['content']['styleLang'],
408 $fileName,
409 $context
410 );
411 $styles['all'] = ( $styles['all'] ?? '' ) . "\n" . $style;
412 }
413 }
414 }
415
416 // Track indirect file dependencies so that ResourceLoaderStartUpModule can check for
417 // on-disk file changes to any of this files without having to recompute the file list
418 $this->saveFileDependencies( $context, $this->localFileRefs );
419
420 return $styles;
421 }
422
427 public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
428 if ( $this->hasGeneratedStyles ) {
429 // Do the default behaviour of returning a url back to load.php
430 // but with only=styles.
431 return parent::getStyleURLsForDebug( $context );
432 }
433 // Our module consists entirely of real css files,
434 // in debug mode we can load those directly.
435 $urls = [];
436 foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
437 $urls[$mediaType] = [];
438 foreach ( $list as $file ) {
439 $urls[$mediaType][] = OutputPage::transformResourcePath(
440 $this->getConfig(),
441 $this->getRemotePath( $file )
442 );
443 }
444 }
445 return $urls;
446 }
447
453 public function getMessages() {
454 return $this->messages;
455 }
456
462 public function getGroup() {
463 return $this->group;
464 }
465
471 public function getDependencies( ResourceLoaderContext $context = null ) {
472 return $this->dependencies;
473 }
474
483 private function getFileContents( $localPath, $type ) {
484 if ( !is_file( $localPath ) ) {
485 throw new RuntimeException(
486 __METHOD__ . ": $type file not found, or is not a file: \"$localPath\""
487 );
488 }
489 return $this->stripBom( file_get_contents( $localPath ) );
490 }
491
497 public function getSkipFunction() {
498 if ( !$this->skipFunction ) {
499 return null;
500 }
501 $localPath = $this->getLocalPath( $this->skipFunction );
502 return $this->getFileContents( $localPath, 'skip function' );
503 }
504
513 public function enableModuleContentVersion() {
514 return false;
515 }
516
523 private function getFileHashes( ResourceLoaderContext $context ) {
524 $files = [];
525
526 $styleFiles = $this->getStyleFiles( $context );
527 foreach ( $styleFiles as $paths ) {
528 $files = array_merge( $files, $paths );
529 }
530
531 // Extract file paths for package files
532 // Optimisation: Use foreach() and isset() instead of array_map/array_filter.
533 // This is a hot code path, called by StartupModule for thousands of modules.
534 $expandedPackageFiles = $this->expandPackageFiles( $context );
535 $packageFiles = [];
536 if ( $expandedPackageFiles ) {
537 foreach ( $expandedPackageFiles['files'] as $fileInfo ) {
538 if ( isset( $fileInfo['filePath'] ) ) {
539 $packageFiles[] = $fileInfo['filePath'];
540 }
541 }
542 }
543
544 // Merge all the file paths we were able discover directly from the module definition.
545 // This is the master list of direct-dependent files for this module.
546 $files = array_merge(
547 $files,
549 $this->scripts,
550 $this->templates,
551 $context->getDebug() ? $this->debugScripts : [],
552 $this->getLanguageScripts( $context->getLanguage() ),
553 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
554 );
555 if ( $this->skipFunction ) {
556 $files[] = $this->skipFunction;
557 }
558
559 // Expand these local paths into absolute file paths
560 $files = array_map( [ $this, 'getLocalPath' ], $files );
561
562 // Add any lazily discovered file dependencies from previous module builds.
563 // These are added last because they are already absolute file paths.
564 $files = array_merge( $files, $this->getFileDependencies( $context ) );
565
566 // Filter out any duplicates. Typically introduced by getFileDependencies() which
567 // may lazily re-discover a master file.
568 $files = array_unique( $files );
569
570 // Don't return array keys or any other form of file path here, only the hashes.
571 // Including file paths would needlessly cause global cache invalidation when files
572 // move on disk or if e.g. the MediaWiki directory name changes.
573 // Anything where order is significant is already detected by the definition summary.
575 }
576
583 public function getDefinitionSummary( ResourceLoaderContext $context ) {
584 $summary = parent::getDefinitionSummary( $context );
585
586 $options = [];
587 foreach ( [
588 // The following properties are omitted because they don't affect the module reponse:
589 // - localBasePath (Per T104950; Changes when absolute directory name changes. If
590 // this affects 'scripts' and other file paths, getFileHashes accounts for that.)
591 // - remoteBasePath (Per T104950)
592 // - dependencies (provided via startup module)
593 // - targets
594 // - group (provided via startup module)
595 'scripts',
596 'debugScripts',
597 'styles',
598 'languageScripts',
599 'skinScripts',
600 'skinStyles',
601 'messages',
602 'templates',
603 'skipFunction',
604 'debugRaw',
605 ] as $member ) {
606 $options[$member] = $this->{$member};
607 }
608
609 $packageFiles = $this->expandPackageFiles( $context );
610 if ( $packageFiles ) {
611 // Extract the minimum needed:
612 // - The 'main' pointer (included as-is).
613 // - The 'files' array, simplified to only which files exist (the keys of
614 // this array), and something that represents their non-file content.
615 // For packaged files that reflect files directly from disk, the
616 // 'getFileHashes' method tracks their content already.
617 // It is important that the keys of the $packageFiles['files'] array
618 // are preserved, as they do affect the module output.
619 $packageFiles['files'] = array_map( function ( $fileInfo ) {
620 return $fileInfo['definitionSummary'] ?? ( $fileInfo['content'] ?? null );
621 }, $packageFiles['files'] );
622 }
623
624 $summary[] = [
625 'options' => $options,
626 'packageFiles' => $packageFiles,
627 'fileHashes' => $this->getFileHashes( $context ),
628 'messageBlob' => $this->getMessageBlob( $context ),
629 ];
630
631 $lessVars = $this->getLessVars( $context );
632 if ( $lessVars ) {
633 $summary[] = [ 'lessVars' => $lessVars ];
634 }
635
636 return $summary;
637 }
638
642 protected function getVueComponentParser() {
643 if ( $this->vueComponentParser === null ) {
644 $this->vueComponentParser = new VueComponentParser;
645 }
647 }
648
653 protected function getPath( $path ) {
654 if ( $path instanceof ResourceLoaderFilePath ) {
655 return $path->getPath();
656 }
657
658 return $path;
659 }
660
665 protected function getLocalPath( $path ) {
666 if ( $path instanceof ResourceLoaderFilePath ) {
667 return $path->getLocalPath();
668 }
669
670 return "{$this->localBasePath}/$path";
671 }
672
677 protected function getRemotePath( $path ) {
678 if ( $path instanceof ResourceLoaderFilePath ) {
679 return $path->getRemotePath();
680 }
681
682 return "{$this->remoteBasePath}/$path";
683 }
684
692 public function getStyleSheetLang( $path ) {
693 return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
694 }
695
701 public static function getPackageFileType( $path ) {
702 if ( preg_match( '/\.json$/i', $path ) ) {
703 return 'data';
704 }
705 if ( preg_match( '/\.vue$/i', $path ) ) {
706 return 'script-vue';
707 }
708 return 'script';
709 }
710
720 protected static function collateFilePathListByOption( array $list, $option, $default ) {
721 $collatedFiles = [];
722 foreach ( $list as $key => $value ) {
723 if ( is_int( $key ) ) {
724 // File name as the value
725 if ( !isset( $collatedFiles[$default] ) ) {
726 $collatedFiles[$default] = [];
727 }
728 $collatedFiles[$default][] = $value;
729 } elseif ( is_array( $value ) ) {
730 // File name as the key, options array as the value
731 $optionValue = $value[$option] ?? $default;
732 if ( !isset( $collatedFiles[$optionValue] ) ) {
733 $collatedFiles[$optionValue] = [];
734 }
735 $collatedFiles[$optionValue][] = $key;
736 }
737 }
738 return $collatedFiles;
739 }
740
750 protected static function tryForKey( array $list, $key, $fallback = null ) {
751 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
752 return $list[$key];
753 } elseif ( is_string( $fallback )
754 && isset( $list[$fallback] )
755 && is_array( $list[$fallback] )
756 ) {
757 return $list[$fallback];
758 }
759 return [];
760 }
761
768 private function getScriptFiles( ResourceLoaderContext $context ) {
769 $files = array_merge(
770 $this->scripts,
771 $this->getLanguageScripts( $context->getLanguage() ),
772 self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
773 );
774 if ( $context->getDebug() ) {
775 $files = array_merge( $files, $this->debugScripts );
776 }
777
778 return array_unique( $files, SORT_REGULAR );
779 }
780
788 private function getLanguageScripts( $lang ) {
789 $scripts = self::tryForKey( $this->languageScripts, $lang );
790 if ( $scripts ) {
791 return $scripts;
792 }
793 $fallbacks = MediaWikiServices::getInstance()->getLanguageFallback()
794 ->getAll( $lang, LanguageFallback::MESSAGES );
795 foreach ( $fallbacks as $lang ) {
796 $scripts = self::tryForKey( $this->languageScripts, $lang );
797 if ( $scripts ) {
798 return $scripts;
799 }
800 }
801
802 return [];
803 }
804
812 public function getStyleFiles( ResourceLoaderContext $context ) {
813 return array_merge_recursive(
814 self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
815 self::collateFilePathListByOption(
816 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
817 'media',
818 'all'
819 )
820 );
821 }
822
830 protected function getSkinStyleFiles( $skinName ) {
832 self::tryForKey( $this->skinStyles, $skinName ),
833 'media',
834 'all'
835 );
836 }
837
844 protected function getAllSkinStyleFiles() {
845 $styleFiles = [];
846 $internalSkinNames = array_keys( Skin::getSkinNames() );
847 $internalSkinNames[] = 'default';
848
849 foreach ( $internalSkinNames as $internalSkinName ) {
850 $styleFiles = array_merge_recursive(
851 $styleFiles,
852 $this->getSkinStyleFiles( $internalSkinName )
853 );
854 }
855
856 return $styleFiles;
857 }
858
864 public function getAllStyleFiles() {
865 $collatedStyleFiles = array_merge_recursive(
866 self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
867 $this->getAllSkinStyleFiles()
868 );
869
870 $result = [];
871
872 foreach ( $collatedStyleFiles as $media => $styleFiles ) {
873 foreach ( $styleFiles as $styleFile ) {
874 $result[] = $this->getLocalPath( $styleFile );
875 }
876 }
877
878 return $result;
879 }
880
888 private function readScriptFiles( array $scripts ) {
889 if ( empty( $scripts ) ) {
890 return '';
891 }
892 $js = '';
893 foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
894 $localPath = $this->getLocalPath( $fileName );
895 $contents = $this->getFileContents( $localPath, 'script' );
896 $js .= $contents . "\n";
897 }
898 return $js;
899 }
900
911 public function readStyleFiles( array $styles, ResourceLoaderContext $context ) {
912 if ( !$styles ) {
913 return [];
914 }
915 foreach ( $styles as $media => $files ) {
916 $uniqueFiles = array_unique( $files, SORT_REGULAR );
917 $styleFiles = [];
918 foreach ( $uniqueFiles as $file ) {
919 $styleFiles[] = $this->readStyleFile( $file, $context );
920 }
921 $styles[$media] = implode( "\n", $styleFiles );
922 }
923 return $styles;
924 }
925
937 protected function readStyleFile( $path, ResourceLoaderContext $context ) {
938 $localPath = $this->getLocalPath( $path );
939 $style = $this->getFileContents( $localPath, 'style' );
940 $styleLang = $this->getStyleSheetLang( $localPath );
941
942 return $this->processStyle( $style, $styleLang, $path, $context );
943 }
944
961 protected function processStyle( $style, $styleLang, $path, ResourceLoaderContext $context ) {
962 $localPath = $this->getLocalPath( $path );
963 $remotePath = $this->getRemotePath( $path );
964
965 if ( $styleLang === 'less' ) {
966 $style = $this->compileLessString( $style, $localPath, $context );
967 $this->hasGeneratedStyles = true;
968 }
969
970 if ( $this->getFlip( $context ) ) {
971 $style = CSSJanus::transform(
972 $style,
973 /* $swapLtrRtlInURL = */ true,
974 /* $swapLeftRightInURL = */ false
975 );
976 }
977
978 $localDir = dirname( $localPath );
979 $remoteDir = dirname( $remotePath );
980 // Get and register local file references
981 $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
982 foreach ( $localFileRefs as $file ) {
983 if ( file_exists( $file ) ) {
984 $this->localFileRefs[] = $file;
985 } else {
986 $this->missingLocalFileRefs[] = $file;
987 }
988 }
989 // Don't cache this call. remap() ensures data URIs embeds are up to date,
990 // and urls contain correct content hashes in their query string. (T128668)
991 return CSSMin::remap( $style, $localDir, $remoteDir, true );
992 }
993
999 public function getFlip( ResourceLoaderContext $context ) {
1000 return $context->getDirection() === 'rtl' && !$this->noflip;
1001 }
1002
1008 public function getTargets() {
1009 return $this->targets;
1010 }
1011
1018 public function getType() {
1019 $canBeStylesOnly = !(
1020 // All options except 'styles', 'skinStyles' and 'debugRaw'
1021 $this->scripts
1022 || $this->debugScripts
1023 || $this->templates
1024 || $this->languageScripts
1025 || $this->skinScripts
1026 || $this->dependencies
1027 || $this->messages
1028 || $this->skipFunction
1030 );
1031 return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
1032 }
1033
1041 protected function compileLessFile( $fileName, ResourceLoaderContext $context ) {
1042 wfDeprecated( __METHOD__, '1.35' );
1043
1044 $style = $this->getFileContents( $fileName, 'LESS' );
1045 return $this->compileLessString( $style, $fileName, $context );
1046 }
1047
1060 protected function compileLessString( $style, $fileName, ResourceLoaderContext $context ) {
1061 static $cache;
1062
1063 if ( !$cache ) {
1064 $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
1065 }
1066
1067 $vars = $this->getLessVars( $context );
1068 // Construct a cache key from a hash of the LESS source, and a hash digest
1069 // of the LESS variables used for compilation.
1070 ksort( $vars );
1071 $varsHash = hash( 'md4', serialize( $vars ) );
1072 $styleHash = hash( 'md4', $style );
1073 $cacheKey = $cache->makeGlobalKey( 'resourceloader-less', $styleHash, $varsHash );
1074 $cachedCompile = $cache->get( $cacheKey );
1075
1076 // If we got a cached value, we have to validate it by getting a
1077 // checksum of all the files that were loaded by the parser and
1078 // ensuring it matches the cached entry's.
1079 if ( isset( $cachedCompile['hash'] ) ) {
1080 $contentHash = FileContentsHasher::getFileContentsHash( $cachedCompile['files'] );
1081 if ( $contentHash === $cachedCompile['hash'] ) {
1082 $this->localFileRefs = array_merge( $this->localFileRefs, $cachedCompile['files'] );
1083 return $cachedCompile['css'];
1084 }
1085 }
1086
1087 $compiler = $context->getResourceLoader()->getLessCompiler( $vars );
1088 $css = $compiler->parse( $style, $fileName )->getCss();
1089 $files = $compiler->AllParsedFiles();
1090 $this->localFileRefs = array_merge( $this->localFileRefs, $files );
1091
1092 $cache->set( $cacheKey, [
1093 'css' => $css,
1094 'files' => $files,
1095 'hash' => FileContentsHasher::getFileContentsHash( $files ),
1096 ], $cache::TTL_DAY );
1097
1098 return $css;
1099 }
1100
1106 public function getTemplates() {
1107 $templates = [];
1108
1109 foreach ( $this->templates as $alias => $templatePath ) {
1110 // Alias is optional
1111 if ( is_int( $alias ) ) {
1112 $alias = $this->getPath( $templatePath );
1113 }
1114 $localPath = $this->getLocalPath( $templatePath );
1115 $content = $this->getFileContents( $localPath, 'template' );
1116
1117 $templates[$alias] = $this->stripBom( $content );
1118 }
1119 return $templates;
1120 }
1121
1141 private function expandPackageFiles( ResourceLoaderContext $context ) {
1142 $hash = $context->getHash();
1143 if ( isset( $this->expandedPackageFiles[$hash] ) ) {
1144 return $this->expandedPackageFiles[$hash];
1145 }
1146 if ( $this->packageFiles === null ) {
1147 return null;
1148 }
1149 $expandedFiles = [];
1150 $mainFile = null;
1151
1152 foreach ( $this->packageFiles as $key => $fileInfo ) {
1153 if ( is_string( $fileInfo ) ) {
1154 $fileInfo = [ 'name' => $fileInfo, 'file' => $fileInfo ];
1155 }
1156 if ( !isset( $fileInfo['name'] ) ) {
1157 $msg = "Missing 'name' key in package file info for module '{$this->getName()}'," .
1158 " offset '{$key}'.";
1159 $this->getLogger()->error( $msg );
1160 throw new LogicException( $msg );
1161 }
1162 $fileName = $fileInfo['name'];
1163
1164 // Infer type from alias if needed
1165 $type = $fileInfo['type'] ?? self::getPackageFileType( $fileName );
1166 $expanded = [ 'type' => $type ];
1167 if ( !empty( $fileInfo['main'] ) ) {
1168 $mainFile = $fileName;
1169 if ( $type !== 'script' && $type !== 'script-vue' ) {
1170 $msg = "Main file in package must be of type 'script', module " .
1171 "'{$this->getName()}', main file '{$mainFile}' is '{$type}'.";
1172 $this->getLogger()->error( $msg );
1173 throw new LogicException( $msg );
1174 }
1175 }
1176
1177 // Perform expansions (except 'file' and 'callback'), creating one of these keys:
1178 // - 'content': literal value.
1179 // - 'filePath': content to be read from a file.
1180 // - 'callback': content computed by a callable.
1181 if ( isset( $fileInfo['content'] ) ) {
1182 $expanded['content'] = $fileInfo['content'];
1183 } elseif ( isset( $fileInfo['file'] ) ) {
1184 $expanded['filePath'] = $fileInfo['file'];
1185 } elseif ( isset( $fileInfo['callback'] ) ) {
1186 // If no extra parameter for the callback is given, use null.
1187 $expanded['callbackParam'] = $fileInfo['callbackParam'] ?? null;
1188
1189 if ( !is_callable( $fileInfo['callback'] ) ) {
1190 $msg = "Invalid 'callback' for module '{$this->getName()}', file '{$fileName}'.";
1191 $this->getLogger()->error( $msg );
1192 throw new LogicException( $msg );
1193 }
1194 if ( isset( $fileInfo['versionCallback'] ) ) {
1195 if ( !is_callable( $fileInfo['versionCallback'] ) ) {
1196 throw new LogicException( "Invalid 'versionCallback' for "
1197 . "module '{$this->getName()}', file '{$fileName}'."
1198 );
1199 }
1200
1201 // Execute the versionCallback with the same arguments that
1202 // would be given to the callback
1203 $callbackResult = ( $fileInfo['versionCallback'] )(
1204 $context,
1205 $this->getConfig(),
1206 $expanded['callbackParam']
1207 );
1208 if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1209 $expanded['filePath'] = $callbackResult->getPath();
1210 } else {
1211 $expanded['definitionSummary'] = $callbackResult;
1212 }
1213 // Don't invoke 'callback' here as it may be expensive (T223260).
1214 $expanded['callback'] = $fileInfo['callback'];
1215 } else {
1216 // Else go ahead invoke callback with its arguments.
1217 $callbackResult = ( $fileInfo['callback'] )(
1218 $context,
1219 $this->getConfig(),
1220 $expanded['callbackParam']
1221 );
1222 if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1223 $expanded['filePath'] = $callbackResult->getPath();
1224 } else {
1225 $expanded['content'] = $callbackResult;
1226 }
1227 }
1228 } elseif ( isset( $fileInfo['config'] ) ) {
1229 if ( $type !== 'data' ) {
1230 $msg = "Key 'config' only valid for data files. "
1231 . " Module '{$this->getName()}', file '{$fileName}' is '{$type}'.";
1232 $this->getLogger()->error( $msg );
1233 throw new LogicException( $msg );
1234 }
1235 $expandedConfig = [];
1236 foreach ( $fileInfo['config'] as $key => $var ) {
1237 $expandedConfig[ is_numeric( $key ) ? $var : $key ] = $this->getConfig()->get( $var );
1238 }
1239 $expanded['content'] = $expandedConfig;
1240 } elseif ( !empty( $fileInfo['main'] ) ) {
1241 // [ 'name' => 'foo.js', 'main' => true ] is shorthand
1242 $expanded['filePath'] = $fileName;
1243 } else {
1244 $msg = "Incomplete definition for module '{$this->getName()}', file '{$fileName}'. "
1245 . "One of 'file', 'content', 'callback', or 'config' must be set.";
1246 $this->getLogger()->error( $msg );
1247 throw new LogicException( $msg );
1248 }
1249
1250 $expandedFiles[$fileName] = $expanded;
1251 }
1252
1253 if ( $expandedFiles && $mainFile === null ) {
1254 // The first package file that is a script is the main file
1255 foreach ( $expandedFiles as $path => $file ) {
1256 if ( $file['type'] === 'script' || $file['type'] === 'script-vue' ) {
1257 $mainFile = $path;
1258 break;
1259 }
1260 }
1261 }
1262
1263 $result = [
1264 'main' => $mainFile,
1265 'files' => $expandedFiles
1266 ];
1267
1268 $this->expandedPackageFiles[$hash] = $result;
1269 return $result;
1270 }
1271
1278 public function getPackageFiles( ResourceLoaderContext $context ) {
1279 if ( $this->packageFiles === null ) {
1280 return null;
1281 }
1282 $hash = $context->getHash();
1283 if ( isset( $this->fullyExpandedPackageFiles[ $hash ] ) ) {
1284 return $this->fullyExpandedPackageFiles[ $hash ];
1285 }
1286 $expandedPackageFiles = $this->expandPackageFiles( $context );
1287
1288 // Expand file contents
1289 foreach ( $expandedPackageFiles['files'] as $fileName => &$fileInfo ) {
1290 // Turn any 'filePath' or 'callback' key into actual 'content',
1291 // and remove the key after that. The callback could return a
1292 // ResourceLoaderFilePath object; if that happens, fall through
1293 // to the 'filePath' handling.
1294 if ( isset( $fileInfo['callback'] ) ) {
1295 $callbackResult = ( $fileInfo['callback'] )(
1296 $context,
1297 $this->getConfig(),
1298 $fileInfo['callbackParam']
1299 );
1300 if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1301 // Fall through to the filePath handling code below
1302 $fileInfo['filePath'] = $callbackResult->getPath();
1303 } else {
1304 $fileInfo['content'] = $callbackResult;
1305 }
1306 unset( $fileInfo['callback'] );
1307 }
1308 // Only interpret 'filePath' if 'content' hasn't been set already.
1309 // This can happen if 'versionCallback' provided 'filePath',
1310 // while 'callback' provides 'content'. In that case both are set
1311 // at this point. The 'filePath' from 'versionCallback' in that case is
1312 // only to inform getDefinitionSummary().
1313 if ( !isset( $fileInfo['content'] ) && isset( $fileInfo['filePath'] ) ) {
1314 $localPath = $this->getLocalPath( $fileInfo['filePath'] );
1315 $content = $this->getFileContents( $localPath, 'package' );
1316 if ( $fileInfo['type'] === 'data' ) {
1317 $content = json_decode( $content );
1318 }
1319 $fileInfo['content'] = $content;
1320 unset( $fileInfo['filePath'] );
1321 }
1322 if ( $fileInfo['type'] === 'script-vue' ) {
1323 try {
1324 $parsedComponent = $this->getVueComponentParser()->parse(
1325 $fileInfo['content'],
1326 [ 'minifyTemplate' => !$context->getDebug() ]
1327 );
1328 } catch ( Exception $e ) {
1329 $msg = "Error parsing file '$fileName' in module '{$this->getName()}': " .
1330 $e->getMessage();
1331 $this->getLogger()->error( $msg );
1332 throw new RuntimeException( $msg );
1333 }
1334 $encodedTemplate = json_encode( $parsedComponent['template'] );
1335 if ( $context->getDebug() ) {
1336 // Replace \n (backslash-n) with space + backslash-newline in debug mode
1337 // We only replace \n if not preceded by a backslash, to avoid breaking '\\n'
1338 $encodedTemplate = preg_replace( '/(?<!\\\\)\\\\n/', " \\\n", $encodedTemplate );
1339 // Expand \t to real tabs in debug mode
1340 $encodedTemplate = strtr( $encodedTemplate, [ "\\t" => "\t" ] );
1341 }
1342 $fileInfo['content'] = [
1343 'script' => $parsedComponent['script'] .
1344 ";\nmodule.exports.template = $encodedTemplate;",
1345 'style' => $parsedComponent['style'] ?? '',
1346 'styleLang' => $parsedComponent['styleLang'] ?? 'css'
1347 ];
1348 $fileInfo['type'] = 'script+style';
1349 }
1350
1351 // Not needed for client response, exists for use by getDefinitionSummary().
1352 unset( $fileInfo['definitionSummary'] );
1353 // Not needed for client response, used by callbacks only.
1354 unset( $fileInfo['callbackParam'] );
1355 }
1356
1357 $this->fullyExpandedPackageFiles[ $hash ] = $expandedPackageFiles;
1358 return $expandedPackageFiles;
1359 }
1360
1371 protected function stripBom( $input ) {
1372 if ( substr_compare( "\xef\xbb\xbf", $input, 0, 3 ) === 0 ) {
1373 return substr( $input, 3 );
1374 }
1375 return $input;
1376 }
1377}
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.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
$fallback
$IP
Definition WebStart.php:49
static getLocalFileReferences( $source, $path)
Get a list of local files referenced in a stylesheet (includes non-existent files).
Definition CSSMin.php:63
static remap( $source, $local, $remote, $embedData=true)
Remaps CSS URL paths and automatically embeds data URIs for CSS rules or url() values preceded by an ...
Definition CSSMin.php:238
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.
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.
getSkipFunction()
Get the skip function.
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()
compileLessString( $style, $fileName, ResourceLoaderContext $context)
Compile a LESS string into CSS.
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.
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 defintion 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.
array $messages
List of message keys used by this module.
string $remoteBasePath
Remote base path, see __construct()
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.
getLessVars(ResourceLoaderContext $context)
Get module-specific LESS variables, if any.
array $contents
Map of (context hash => cached module content)
saveFileDependencies(ResourceLoaderContext $context, array $curFileRefs)
Save the indirect dependencies for this module persuant to the skin/language context.
Parser for Vue single file components (.vue files).
const CACHE_ANYTHING
Definition Defines.php:91
$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