MediaWiki  master
ResourceLoaderFileModule.php
Go to the documentation of this file.
1 <?php
25 
40 
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 
117  private $expandedPackageFiles = [];
118 
124 
132  protected $dependencies = [];
133 
137  protected $skipFunction = null;
138 
146  protected $messages = [];
147 
149  protected $group;
150 
152  protected $debugRaw = true;
153 
154  protected $targets = [ 'desktop' ];
155 
157  protected $noflip = false;
158 
163  protected $hasGeneratedStyles = false;
164 
172  protected $localFileRefs = [];
173 
178  protected $missingLocalFileRefs = [];
179 
183  protected $vueComponentParser = null;
184 
196  public function __construct(
197  array $options = [],
198  $localBasePath = null,
199  $remoteBasePath = null
200  ) {
201  // Flag to decide whether to automagically add the mediawiki.template module
202  $hasTemplates = false;
203  // localBasePath and remoteBasePath both have unbelievably long fallback chains
204  // and need to be handled separately.
205  list( $this->localBasePath, $this->remoteBasePath ) =
207 
208  // Extract, validate and normalise remaining options
209  foreach ( $options as $member => $option ) {
210  switch ( $member ) {
211  // Lists of file paths
212  case 'scripts':
213  case 'debugScripts':
214  case 'styles':
215  case 'packageFiles':
216  $this->{$member} = is_array( $option ) ? $option : [ $option ];
217  break;
218  case 'templates':
219  $hasTemplates = true;
220  $this->{$member} = is_array( $option ) ? $option : [ $option ];
221  break;
222  // Collated lists of file paths
223  case 'languageScripts':
224  case 'skinScripts':
225  case 'skinStyles':
226  if ( !is_array( $option ) ) {
227  throw new InvalidArgumentException(
228  "Invalid collated file path list error. " .
229  "'$option' given, array expected."
230  );
231  }
232  foreach ( $option as $key => $value ) {
233  if ( !is_string( $key ) ) {
234  throw new InvalidArgumentException(
235  "Invalid collated file path list key error. " .
236  "'$key' given, string expected."
237  );
238  }
239  $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
240  }
241  break;
242  case 'deprecated':
243  $this->deprecated = $option;
244  break;
245  // Lists of strings
246  case 'dependencies':
247  case 'messages':
248  case 'targets':
249  // Normalise
250  $option = array_values( array_unique( (array)$option ) );
251  sort( $option );
252 
253  $this->{$member} = $option;
254  break;
255  // Single strings
256  case 'group':
257  case 'skipFunction':
258  $this->{$member} = (string)$option;
259  break;
260  // Single booleans
261  case 'debugRaw':
262  case 'noflip':
263  $this->{$member} = (bool)$option;
264  break;
265  }
266  }
267  if ( isset( $options['scripts'] ) && isset( $options['packageFiles'] ) ) {
268  throw new InvalidArgumentException( "A module may not set both 'scripts' and 'packageFiles'" );
269  }
270  if ( $hasTemplates ) {
271  $this->dependencies[] = 'mediawiki.template';
272  // Ensure relevant template compiler module gets loaded
273  foreach ( $this->templates as $alias => $templatePath ) {
274  if ( is_int( $alias ) ) {
275  $alias = $this->getPath( $templatePath );
276  }
277  $suffix = explode( '.', $alias );
278  $suffix = end( $suffix );
279  $compilerModule = 'mediawiki.template.' . $suffix;
280  if ( $suffix !== 'html' && !in_array( $compilerModule, $this->dependencies ) ) {
281  $this->dependencies[] = $compilerModule;
282  }
283  }
284  }
285  }
286 
298  public static function extractBasePaths(
299  array $options = [],
300  $localBasePath = null,
301  $remoteBasePath = null
302  ) {
303  global $IP, $wgResourceBasePath;
304 
305  // The different ways these checks are done, and their ordering, look very silly,
306  // but were preserved for backwards-compatibility just in case. Tread lightly.
307 
308  if ( $localBasePath === null ) {
310  }
311  if ( $remoteBasePath === null ) {
313  }
314 
315  if ( isset( $options['remoteExtPath'] ) ) {
316  global $wgExtensionAssetsPath;
317  $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
318  }
319 
320  if ( isset( $options['remoteSkinPath'] ) ) {
321  global $wgStylePath;
322  $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
323  }
324 
325  if ( array_key_exists( 'localBasePath', $options ) ) {
326  $localBasePath = (string)$options['localBasePath'];
327  }
328 
329  if ( array_key_exists( 'remoteBasePath', $options ) ) {
330  $remoteBasePath = (string)$options['remoteBasePath'];
331  }
332 
333  return [ $localBasePath, $remoteBasePath ];
334  }
335 
343  $deprecationScript = $this->getDeprecationInformation( $context );
344  if ( $this->packageFiles !== null ) {
345  $packageFiles = $this->getPackageFiles( $context );
346  foreach ( $packageFiles['files'] as &$file ) {
347  if ( $file['type'] === 'script+style' ) {
348  $file['content'] = $file['content']['script'];
349  $file['type'] = 'script';
350  }
351  }
352  if ( $deprecationScript ) {
353  $mainFile =& $packageFiles['files'][$packageFiles['main']];
354  $mainFile['content'] = $deprecationScript . $mainFile['content'];
355  }
356  return $packageFiles;
357  }
358 
359  $files = $this->getScriptFiles( $context );
360  return $deprecationScript . $this->readScriptFiles( $files );
361  }
362 
368  $urls = [];
369  foreach ( $this->getScriptFiles( $context ) as $file ) {
371  $this->getConfig(),
372  $this->getRemotePath( $file )
373  );
374  }
375  return $urls;
376  }
377 
381  public function supportsURLLoading() {
382  // If package files are involved, don't support URL loading, because that breaks
383  // scoped require() functions
384  return $this->debugRaw && !$this->packageFiles;
385  }
386 
394  $styles = $this->readStyleFiles(
395  $this->getStyleFiles( $context ),
396  $context
397  );
398 
399  if ( $this->packageFiles !== null ) {
400  $packageFiles = $this->getPackageFiles( $context );
401  foreach ( $packageFiles['files'] as $fileName => $file ) {
402  if ( $file['type'] === 'script+style' ) {
403  $style = $this->processStyle(
404  $file['content']['style'],
405  $file['content']['styleLang'],
406  $fileName,
407  $context
408  );
409  $styles['all'] = ( $styles['all'] ?? '' ) . "\n" . $style;
410  }
411  }
412  }
413 
414  // Track indirect file dependencies so that ResourceLoaderStartUpModule can check for
415  // on-disk file changes to any of this files without having to recompute the file list
416  $this->saveFileDependencies( $context, $this->localFileRefs );
417 
418  return $styles;
419  }
420 
426  if ( $this->hasGeneratedStyles ) {
427  // Do the default behaviour of returning a url back to load.php
428  // but with only=styles.
429  return parent::getStyleURLsForDebug( $context );
430  }
431  // Our module consists entirely of real css files,
432  // in debug mode we can load those directly.
433  $urls = [];
434  foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
435  $urls[$mediaType] = [];
436  foreach ( $list as $file ) {
438  $this->getConfig(),
439  $this->getRemotePath( $file )
440  );
441  }
442  }
443  return $urls;
444  }
445 
451  public function getMessages() {
452  return $this->messages;
453  }
454 
460  public function getGroup() {
461  return $this->group;
462  }
463 
469  public function getDependencies( ResourceLoaderContext $context = null ) {
470  return $this->dependencies;
471  }
472 
481  private function getFileContents( $localPath, $type ) {
482  if ( !is_file( $localPath ) ) {
483  throw new RuntimeException(
484  __METHOD__ . ": $type file not found, or is not a file: \"$localPath\""
485  );
486  }
487  return $this->stripBom( file_get_contents( $localPath ) );
488  }
489 
495  public function getSkipFunction() {
496  if ( !$this->skipFunction ) {
497  return null;
498  }
499  $localPath = $this->getLocalPath( $this->skipFunction );
500  return $this->getFileContents( $localPath, 'skip function' );
501  }
502 
511  public function enableModuleContentVersion() {
512  return false;
513  }
514 
522  $files = [];
523 
524  // Flatten style files into $files
525  $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' );
526  foreach ( $styles as $styleFiles ) {
527  $files = array_merge( $files, $styleFiles );
528  }
529 
531  self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
532  'media',
533  'all'
534  );
535  foreach ( $skinFiles as $styleFiles ) {
536  $files = array_merge( $files, $styleFiles );
537  }
538 
539  // Extract file paths for package files
540  // Optimisation: Use foreach() and isset() instead of array_map/array_filter.
541  // This is a hot code path, called by StartupModule for thousands of modules.
542  $expandedPackageFiles = $this->expandPackageFiles( $context );
543  $packageFiles = [];
544  if ( $expandedPackageFiles ) {
545  foreach ( $expandedPackageFiles['files'] as $fileInfo ) {
546  if ( isset( $fileInfo['filePath'] ) ) {
547  $packageFiles[] = $fileInfo['filePath'];
548  }
549  }
550  }
551 
552  // Merge all the file paths we were able discover directly from the module definition.
553  // This is the master list of direct-dependent files for this module.
554  $files = array_merge(
555  $files,
557  $this->scripts,
558  $this->templates,
559  $context->getDebug() ? $this->debugScripts : [],
560  $this->getLanguageScripts( $context->getLanguage() ),
561  self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
562  );
563  if ( $this->skipFunction ) {
564  $files[] = $this->skipFunction;
565  }
566 
567  // Expand these local paths into absolute file paths
568  $files = array_map( [ $this, 'getLocalPath' ], $files );
569 
570  // Add any lazily discovered file dependencies from previous module builds.
571  // These are added last because they are already absolute file paths.
572  $files = array_merge( $files, $this->getFileDependencies( $context ) );
573 
574  // Filter out any duplicates. Typically introduced by getFileDependencies() which
575  // may lazily re-discover a master file.
576  $files = array_unique( $files );
577 
578  // Don't return array keys or any other form of file path here, only the hashes.
579  // Including file paths would needlessly cause global cache invalidation when files
580  // move on disk or if e.g. the MediaWiki directory name changes.
581  // Anything where order is significant is already detected by the definition summary.
583  }
584 
592  $summary = parent::getDefinitionSummary( $context );
593 
594  $options = [];
595  foreach ( [
596  // The following properties are omitted because they don't affect the module reponse:
597  // - localBasePath (Per T104950; Changes when absolute directory name changes. If
598  // this affects 'scripts' and other file paths, getFileHashes accounts for that.)
599  // - remoteBasePath (Per T104950)
600  // - dependencies (provided via startup module)
601  // - targets
602  // - group (provided via startup module)
603  'scripts',
604  'debugScripts',
605  'styles',
606  'languageScripts',
607  'skinScripts',
608  'skinStyles',
609  'messages',
610  'templates',
611  'skipFunction',
612  'debugRaw',
613  ] as $member ) {
614  $options[$member] = $this->{$member};
615  }
616 
617  $packageFiles = $this->expandPackageFiles( $context );
618  if ( $packageFiles ) {
619  // Extract the minimum needed:
620  // - The 'main' pointer (included as-is).
621  // - The 'files' array, simplified to only which files exist (the keys of
622  // this array), and something that represents their non-file content.
623  // For packaged files that reflect files directly from disk, the
624  // 'getFileHashes' method tracks their content already.
625  // It is important that the keys of the $packageFiles['files'] array
626  // are preserved, as they do affect the module output.
627  $packageFiles['files'] = array_map( function ( $fileInfo ) {
628  return $fileInfo['definitionSummary'] ?? ( $fileInfo['content'] ?? null );
629  }, $packageFiles['files'] );
630  }
631 
632  $summary[] = [
633  'options' => $options,
634  'packageFiles' => $packageFiles,
635  'fileHashes' => $this->getFileHashes( $context ),
636  'messageBlob' => $this->getMessageBlob( $context ),
637  ];
638 
639  $lessVars = $this->getLessVars( $context );
640  if ( $lessVars ) {
641  $summary[] = [ 'lessVars' => $lessVars ];
642  }
643 
644  return $summary;
645  }
646 
650  protected function getVueComponentParser() {
651  if ( $this->vueComponentParser === null ) {
652  $this->vueComponentParser = new VueComponentParser;
653  }
655  }
656 
661  protected function getPath( $path ) {
662  if ( $path instanceof ResourceLoaderFilePath ) {
663  return $path->getPath();
664  }
665 
666  return $path;
667  }
668 
673  protected function getLocalPath( $path ) {
674  if ( $path instanceof ResourceLoaderFilePath ) {
675  return $path->getLocalPath();
676  }
677 
678  return "{$this->localBasePath}/$path";
679  }
680 
685  protected function getRemotePath( $path ) {
686  if ( $path instanceof ResourceLoaderFilePath ) {
687  return $path->getRemotePath();
688  }
689 
690  return "{$this->remoteBasePath}/$path";
691  }
692 
700  public function getStyleSheetLang( $path ) {
701  return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
702  }
703 
709  public static function getPackageFileType( $path ) {
710  if ( preg_match( '/\.json$/i', $path ) ) {
711  return 'data';
712  }
713  if ( preg_match( '/\.vue$/i', $path ) ) {
714  return 'script-vue';
715  }
716  return 'script';
717  }
718 
728  protected static function collateFilePathListByOption( array $list, $option, $default ) {
729  $collatedFiles = [];
730  foreach ( (array)$list as $key => $value ) {
731  if ( is_int( $key ) ) {
732  // File name as the value
733  if ( !isset( $collatedFiles[$default] ) ) {
734  $collatedFiles[$default] = [];
735  }
736  $collatedFiles[$default][] = $value;
737  } elseif ( is_array( $value ) ) {
738  // File name as the key, options array as the value
739  $optionValue = $value[$option] ?? $default;
740  if ( !isset( $collatedFiles[$optionValue] ) ) {
741  $collatedFiles[$optionValue] = [];
742  }
743  $collatedFiles[$optionValue][] = $key;
744  }
745  }
746  return $collatedFiles;
747  }
748 
758  protected static function tryForKey( array $list, $key, $fallback = null ) {
759  if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
760  return $list[$key];
761  } elseif ( is_string( $fallback )
762  && isset( $list[$fallback] )
763  && is_array( $list[$fallback] )
764  ) {
765  return $list[$fallback];
766  }
767  return [];
768  }
769 
777  $files = array_merge(
778  $this->scripts,
779  $this->getLanguageScripts( $context->getLanguage() ),
780  self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
781  );
782  if ( $context->getDebug() ) {
783  $files = array_merge( $files, $this->debugScripts );
784  }
785 
786  return array_unique( $files, SORT_REGULAR );
787  }
788 
796  private function getLanguageScripts( $lang ) {
797  $scripts = self::tryForKey( $this->languageScripts, $lang );
798  if ( $scripts ) {
799  return $scripts;
800  }
801  $fallbacks = MediaWikiServices::getInstance()->getLanguageFallback()
802  ->getAll( $lang, LanguageFallback::MESSAGES );
803  foreach ( $fallbacks as $lang ) {
804  $scripts = self::tryForKey( $this->languageScripts, $lang );
805  if ( $scripts ) {
806  return $scripts;
807  }
808  }
809 
810  return [];
811  }
812 
821  return array_merge_recursive(
822  self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
823  self::collateFilePathListByOption(
824  self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
825  'media',
826  'all'
827  )
828  );
829  }
830 
838  protected function getSkinStyleFiles( $skinName ) {
840  self::tryForKey( $this->skinStyles, $skinName ),
841  'media',
842  'all'
843  );
844  }
845 
852  protected function getAllSkinStyleFiles() {
853  $styleFiles = [];
854  $internalSkinNames = array_keys( Skin::getSkinNames() );
855  $internalSkinNames[] = 'default';
856 
857  foreach ( $internalSkinNames as $internalSkinName ) {
858  $styleFiles = array_merge_recursive(
859  $styleFiles,
860  $this->getSkinStyleFiles( $internalSkinName )
861  );
862  }
863 
864  return $styleFiles;
865  }
866 
872  public function getAllStyleFiles() {
873  $collatedStyleFiles = array_merge_recursive(
874  self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
875  $this->getAllSkinStyleFiles()
876  );
877 
878  $result = [];
879 
880  foreach ( $collatedStyleFiles as $media => $styleFiles ) {
881  foreach ( $styleFiles as $styleFile ) {
882  $result[] = $this->getLocalPath( $styleFile );
883  }
884  }
885 
886  return $result;
887  }
888 
896  private function readScriptFiles( array $scripts ) {
897  if ( empty( $scripts ) ) {
898  return '';
899  }
900  $js = '';
901  foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
902  $localPath = $this->getLocalPath( $fileName );
903  $contents = $this->getFileContents( $localPath, 'script' );
904  $js .= $contents . "\n";
905  }
906  return $js;
907  }
908 
920  if ( !$styles ) {
921  return [];
922  }
923  foreach ( $styles as $media => $files ) {
924  $uniqueFiles = array_unique( $files, SORT_REGULAR );
925  $styleFiles = [];
926  foreach ( $uniqueFiles as $file ) {
927  $styleFiles[] = $this->readStyleFile( $file, $context );
928  }
929  $styles[$media] = implode( "\n", $styleFiles );
930  }
931  return $styles;
932  }
933 
946  $localPath = $this->getLocalPath( $path );
947  $style = $this->getFileContents( $localPath, 'style' );
948  $styleLang = $this->getStyleSheetLang( $localPath );
949 
950  return $this->processStyle( $style, $styleLang, $path, $context );
951  }
952 
969  protected function processStyle( $style, $styleLang, $path, ResourceLoaderContext $context ) {
970  $localPath = $this->getLocalPath( $path );
971  $remotePath = $this->getRemotePath( $path );
972 
973  if ( $styleLang === 'less' ) {
974  $style = $this->compileLessString( $style, $localPath, $context );
975  $this->hasGeneratedStyles = true;
976  }
977 
978  if ( $this->getFlip( $context ) ) {
979  $style = CSSJanus::transform(
980  $style,
981  /* $swapLtrRtlInURL = */ true,
982  /* $swapLeftRightInURL = */ false
983  );
984  }
985 
986  $localDir = dirname( $localPath );
987  $remoteDir = dirname( $remotePath );
988  // Get and register local file references
989  $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
990  foreach ( $localFileRefs as $file ) {
991  if ( file_exists( $file ) ) {
992  $this->localFileRefs[] = $file;
993  } else {
994  $this->missingLocalFileRefs[] = $file;
995  }
996  }
997  // Don't cache this call. remap() ensures data URIs embeds are up to date,
998  // and urls contain correct content hashes in their query string. (T128668)
999  return CSSMin::remap( $style, $localDir, $remoteDir, true );
1000  }
1001 
1008  return $context->getDirection() === 'rtl' && !$this->noflip;
1009  }
1010 
1016  public function getTargets() {
1017  return $this->targets;
1018  }
1019 
1026  public function getType() {
1027  $canBeStylesOnly = !(
1028  // All options except 'styles', 'skinStyles' and 'debugRaw'
1029  $this->scripts
1030  || $this->debugScripts
1031  || $this->templates
1032  || $this->languageScripts
1033  || $this->skinScripts
1034  || $this->dependencies
1035  || $this->messages
1036  || $this->skipFunction
1038  );
1039  return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
1040  }
1041 
1049  protected function compileLessFile( $fileName, ResourceLoaderContext $context ) {
1050  wfDeprecated( __METHOD__, '1.35' );
1051 
1052  $style = $this->getFileContents( $fileName, 'LESS' );
1053  return $this->compileLessString( $style, $fileName, $context );
1054  }
1055 
1068  protected function compileLessString( $style, $fileName, ResourceLoaderContext $context ) {
1069  static $cache;
1070 
1071  if ( !$cache ) {
1073  }
1074 
1075  $vars = $this->getLessVars( $context );
1076  // Construct a cache key from a hash of the LESS source, and a hash digest
1077  // of the LESS variables used for compilation.
1078  ksort( $vars );
1079  $varsHash = hash( 'md4', serialize( $vars ) );
1080  $styleHash = hash( 'md4', $style );
1081  $cacheKey = $cache->makeGlobalKey( 'resourceloader-less', $styleHash, $varsHash );
1082  $cachedCompile = $cache->get( $cacheKey );
1083 
1084  // If we got a cached value, we have to validate it by getting a
1085  // checksum of all the files that were loaded by the parser and
1086  // ensuring it matches the cached entry's.
1087  if ( isset( $cachedCompile['hash'] ) ) {
1088  $contentHash = FileContentsHasher::getFileContentsHash( $cachedCompile['files'] );
1089  if ( $contentHash === $cachedCompile['hash'] ) {
1090  $this->localFileRefs = array_merge( $this->localFileRefs, $cachedCompile['files'] );
1091  return $cachedCompile['css'];
1092  }
1093  }
1094 
1095  $compiler = $context->getResourceLoader()->getLessCompiler( $vars );
1096  $css = $compiler->parse( $style, $fileName )->getCss();
1097  $files = $compiler->AllParsedFiles();
1098  $this->localFileRefs = array_merge( $this->localFileRefs, $files );
1099 
1100  $cache->set( $cacheKey, [
1101  'css' => $css,
1102  'files' => $files,
1103  'hash' => FileContentsHasher::getFileContentsHash( $files ),
1104  ], $cache::TTL_DAY );
1105 
1106  return $css;
1107  }
1108 
1114  public function getTemplates() {
1115  $templates = [];
1116 
1117  foreach ( $this->templates as $alias => $templatePath ) {
1118  // Alias is optional
1119  if ( is_int( $alias ) ) {
1120  $alias = $this->getPath( $templatePath );
1121  }
1122  $localPath = $this->getLocalPath( $templatePath );
1123  $content = $this->getFileContents( $localPath, 'template' );
1124 
1125  $templates[$alias] = $this->stripBom( $content );
1126  }
1127  return $templates;
1128  }
1129 
1150  $hash = $context->getHash();
1151  if ( isset( $this->expandedPackageFiles[$hash] ) ) {
1152  return $this->expandedPackageFiles[$hash];
1153  }
1154  if ( $this->packageFiles === null ) {
1155  return null;
1156  }
1157  $expandedFiles = [];
1158  $mainFile = null;
1159 
1160  foreach ( $this->packageFiles as $key => $fileInfo ) {
1161  if ( is_string( $fileInfo ) ) {
1162  $fileInfo = [ 'name' => $fileInfo, 'file' => $fileInfo ];
1163  }
1164  if ( !isset( $fileInfo['name'] ) ) {
1165  $msg = "Missing 'name' key in package file info for module '{$this->getName()}'," .
1166  " offset '{$key}'.";
1167  $this->getLogger()->error( $msg );
1168  throw new LogicException( $msg );
1169  }
1170  $fileName = $fileInfo['name'];
1171 
1172  // Infer type from alias if needed
1173  $type = $fileInfo['type'] ?? self::getPackageFileType( $fileName );
1174  $expanded = [ 'type' => $type ];
1175  if ( !empty( $fileInfo['main'] ) ) {
1176  $mainFile = $fileName;
1177  if ( $type !== 'script' && $type !== 'script-vue' ) {
1178  $msg = "Main file in package must be of type 'script', module " .
1179  "'{$this->getName()}', main file '{$mainFile}' is '{$type}'.";
1180  $this->getLogger()->error( $msg );
1181  throw new LogicException( $msg );
1182  }
1183  }
1184 
1185  // Perform expansions (except 'file' and 'callback'), creating one of these keys:
1186  // - 'content': literal value.
1187  // - 'filePath': content to be read from a file.
1188  // - 'callback': content computed by a callable.
1189  if ( isset( $fileInfo['content'] ) ) {
1190  $expanded['content'] = $fileInfo['content'];
1191  } elseif ( isset( $fileInfo['file'] ) ) {
1192  $expanded['filePath'] = $fileInfo['file'];
1193  } elseif ( isset( $fileInfo['callback'] ) ) {
1194  // If no extra parameter for the callback is given, use null.
1195  $expanded['callbackParam'] = $fileInfo['callbackParam'] ?? null;
1196 
1197  if ( !is_callable( $fileInfo['callback'] ) ) {
1198  $msg = "Invalid 'callback' for module '{$this->getName()}', file '{$fileName}'.";
1199  $this->getLogger()->error( $msg );
1200  throw new LogicException( $msg );
1201  }
1202  if ( isset( $fileInfo['versionCallback'] ) ) {
1203  if ( !is_callable( $fileInfo['versionCallback'] ) ) {
1204  throw new LogicException( "Invalid 'versionCallback' for "
1205  . "module '{$this->getName()}', file '{$fileName}'."
1206  );
1207  }
1208 
1209  // Execute the versionCallback with the same arguments that
1210  // would be given to the callback
1211  $callbackResult = ( $fileInfo['versionCallback'] )(
1212  $context,
1213  $this->getConfig(),
1214  $expanded['callbackParam']
1215  );
1216  if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1217  $expanded['filePath'] = $callbackResult->getPath();
1218  } else {
1219  $expanded['definitionSummary'] = $callbackResult;
1220  }
1221  // Don't invoke 'callback' here as it may be expensive (T223260).
1222  $expanded['callback'] = $fileInfo['callback'];
1223  } else {
1224  // Else go ahead invoke callback with its arguments.
1225  $callbackResult = ( $fileInfo['callback'] )(
1226  $context,
1227  $this->getConfig(),
1228  $expanded['callbackParam']
1229  );
1230  if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1231  $expanded['filePath'] = $callbackResult->getPath();
1232  } else {
1233  $expanded['content'] = $callbackResult;
1234  }
1235  }
1236  } elseif ( isset( $fileInfo['config'] ) ) {
1237  if ( $type !== 'data' ) {
1238  $msg = "Key 'config' only valid for data files. "
1239  . " Module '{$this->getName()}', file '{$fileName}' is '{$type}'.";
1240  $this->getLogger()->error( $msg );
1241  throw new LogicException( $msg );
1242  }
1243  $expandedConfig = [];
1244  foreach ( $fileInfo['config'] as $key => $var ) {
1245  $expandedConfig[ is_numeric( $key ) ? $var : $key ] = $this->getConfig()->get( $var );
1246  }
1247  $expanded['content'] = $expandedConfig;
1248  } elseif ( !empty( $fileInfo['main'] ) ) {
1249  // [ 'name' => 'foo.js', 'main' => true ] is shorthand
1250  $expanded['filePath'] = $fileName;
1251  } else {
1252  $msg = "Incomplete definition for module '{$this->getName()}', file '{$fileName}'. "
1253  . "One of 'file', 'content', 'callback', or 'config' must be set.";
1254  $this->getLogger()->error( $msg );
1255  throw new LogicException( $msg );
1256  }
1257 
1258  $expandedFiles[$fileName] = $expanded;
1259  }
1260 
1261  if ( $expandedFiles && $mainFile === null ) {
1262  // The first package file that is a script is the main file
1263  foreach ( $expandedFiles as $path => $file ) {
1264  if ( $file['type'] === 'script' || $file['type'] === 'script-vue' ) {
1265  $mainFile = $path;
1266  break;
1267  }
1268  }
1269  }
1270 
1271  $result = [
1272  'main' => $mainFile,
1273  'files' => $expandedFiles
1274  ];
1275 
1276  $this->expandedPackageFiles[$hash] = $result;
1277  return $result;
1278  }
1279 
1287  if ( $this->packageFiles === null ) {
1288  return null;
1289  }
1290  $hash = $context->getHash();
1291  if ( isset( $this->fullyExpandedPackageFiles[ $hash ] ) ) {
1292  return $this->fullyExpandedPackageFiles[ $hash ];
1293  }
1294  $expandedPackageFiles = $this->expandPackageFiles( $context );
1295 
1296  // Expand file contents
1297  foreach ( $expandedPackageFiles['files'] as $fileName => &$fileInfo ) {
1298  // Turn any 'filePath' or 'callback' key into actual 'content',
1299  // and remove the key after that. The callback could return a
1300  // ResourceLoaderFilePath object; if that happens, fall through
1301  // to the 'filePath' handling.
1302  if ( isset( $fileInfo['callback'] ) ) {
1303  $callbackResult = ( $fileInfo['callback'] )(
1304  $context,
1305  $this->getConfig(),
1306  $fileInfo['callbackParam']
1307  );
1308  if ( $callbackResult instanceof ResourceLoaderFilePath ) {
1309  // Fall through to the filePath handling code below
1310  $fileInfo['filePath'] = $callbackResult->getPath();
1311  } else {
1312  $fileInfo['content'] = $callbackResult;
1313  }
1314  unset( $fileInfo['callback'] );
1315  }
1316  // Only interpret 'filePath' if 'content' hasn't been set already.
1317  // This can happen if 'versionCallback' provided 'filePath',
1318  // while 'callback' provides 'content'. In that case both are set
1319  // at this point. The 'filePath' from 'versionCallback' in that case is
1320  // only to inform getDefinitionSummary().
1321  if ( !isset( $fileInfo['content'] ) && isset( $fileInfo['filePath'] ) ) {
1322  $localPath = $this->getLocalPath( $fileInfo['filePath'] );
1323  $content = $this->getFileContents( $localPath, 'package' );
1324  if ( $fileInfo['type'] === 'data' ) {
1325  $content = json_decode( $content );
1326  }
1327  $fileInfo['content'] = $content;
1328  unset( $fileInfo['filePath'] );
1329  }
1330  if ( $fileInfo['type'] === 'script-vue' ) {
1331  try {
1332  $parsedComponent = $this->getVueComponentParser()->parse( $fileInfo['content'] );
1333  } catch ( Exception $e ) {
1334  $msg = "Error parsing file '$fileName' in module '{$this->getName()}': " .
1335  $e->getMessage();
1336  $this->getLogger()->error( $msg );
1337  throw new RuntimeException( $msg );
1338  }
1339  $template = $context->getDebug() ?
1340  $parsedComponent['rawTemplate'] :
1341  $parsedComponent['template'];
1342  $encodedTemplate = json_encode( $template );
1343  if ( $context->getDebug() ) {
1344  // Replace \n (backslash-n) with space + backslash-newline in debug mode
1345  // We only replace \n if not preceded by a backslash, to avoid breaking '\\n'
1346  $encodedTemplate = preg_replace( '/(?<!\\\\)\\\\n/', " \\\n", $encodedTemplate );
1347  // Expand \t to real tabs in debug mode
1348  $encodedTemplate = strtr( $encodedTemplate, [ "\\t" => "\t" ] );
1349  }
1350  $fileInfo['content'] = [
1351  'script' => $parsedComponent['script'] .
1352  ";\nmodule.exports.template = $encodedTemplate;",
1353  'style' => $parsedComponent['style'] ?? '',
1354  'styleLang' => $parsedComponent['styleLang'] ?? 'css'
1355  ];
1356  $fileInfo['type'] = 'script+style';
1357  }
1358 
1359  // Not needed for client response, exists for use by getDefinitionSummary().
1360  unset( $fileInfo['definitionSummary'] );
1361  // Not needed for client response, used by callbacks only.
1362  unset( $fileInfo['callbackParam'] );
1363  }
1364 
1365  $this->fullyExpandedPackageFiles[ $hash ] = $expandedPackageFiles;
1366  return $expandedPackageFiles;
1367  }
1368 
1379  protected function stripBom( $input ) {
1380  if ( substr_compare( "\xef\xbb\xbf", $input, 0, 3 ) === 0 ) {
1381  return substr( $input, 3 );
1382  }
1383  return $input;
1384  }
1385 }
ResourceLoaderFileModule\getSkipFunction
getSkipFunction()
Get the skip function.
Definition: ResourceLoaderFileModule.php:495
ResourceLoaderContext
Context object that contains information about the state of a specific ResourceLoader web request.
Definition: ResourceLoaderContext.php:33
ResourceLoaderFileModule\$styles
array $styles
List of paths to CSS files to always include.
Definition: ResourceLoaderFileModule.php:93
ResourceLoaderFileModule\$skinScripts
array $skinScripts
List of JavaScript files to include when using a specific skin.
Definition: ResourceLoaderFileModule.php:75
ResourceLoaderFileModule\$debugScripts
array $debugScripts
List of paths to JavaScript files to include in debug mode.
Definition: ResourceLoaderFileModule.php:84
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:146
$lang
if(!isset( $args[0])) $lang
Definition: testCompression.php:37
ResourceLoaderFileModule\$templates
array $templates
Saves a list of the templates named by the modules.
Definition: ResourceLoaderFileModule.php:48
CSSMin\remap
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
ResourceLoaderModule\$contents
array $contents
Map of (context hash => cached module content)
Definition: ResourceLoaderModule.php:65
ResourceLoaderFileModule\getStyles
getStyles(ResourceLoaderContext $context)
Get all styles for a given context.
Definition: ResourceLoaderFileModule.php:393
$fallback
$fallback
Definition: MessagesAb.php:11
ResourceLoaderFileModule\getFileHashes
getFileHashes(ResourceLoaderContext $context)
Helper method for getDefinitionSummary.
Definition: ResourceLoaderFileModule.php:521
ResourceLoaderFileModule\$noflip
bool $noflip
Whether CSSJanus flipping should be skipped for this module.
Definition: ResourceLoaderFileModule.php:157
ResourceLoaderFileModule\getScriptURLsForDebug
getScriptURLsForDebug(ResourceLoaderContext $context)
Definition: ResourceLoaderFileModule.php:367
ResourceLoaderFileModule\__construct
__construct(array $options=[], $localBasePath=null, $remoteBasePath=null)
Constructs a new module from an options array.
Definition: ResourceLoaderFileModule.php:196
ResourceLoaderFilePath
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path,...
Definition: ResourceLoaderFilePath.php:28
MediaWiki\Languages\LanguageFallback
Definition: LanguageFallback.php:31
ResourceLoaderFileModule\enableModuleContentVersion
enableModuleContentVersion()
Disable module content versioning.
Definition: ResourceLoaderFileModule.php:511
$file
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
ResourceLoaderFileModule\$debugRaw
bool $debugRaw
Link to raw files in debug mode.
Definition: ResourceLoaderFileModule.php:152
$wgExtensionAssetsPath
$wgExtensionAssetsPath
The URL path of the extensions directory.
Definition: DefaultSettings.php:222
ResourceLoaderFileModule\$hasGeneratedStyles
bool $hasGeneratedStyles
Whether getStyleURLsForDebug should return raw file paths, or return load.php urls.
Definition: ResourceLoaderFileModule.php:163
ResourceLoaderFileModule\compileLessString
compileLessString( $style, $fileName, ResourceLoaderContext $context)
Compile a LESS string into CSS.
Definition: ResourceLoaderFileModule.php:1068
ResourceLoaderFileModule\$dependencies
array $dependencies
List of modules this module depends on.
Definition: ResourceLoaderFileModule.php:132
ResourceLoaderFileModule\$skinStyles
array $skinStyles
List of paths to CSS files to include when using specific skins.
Definition: ResourceLoaderFileModule.php:102
serialize
serialize()
Definition: ApiMessageTrait.php:138
ResourceLoaderFileModule\getFlip
getFlip(ResourceLoaderContext $context)
Get whether CSS for this module should be flipped.
Definition: ResourceLoaderFileModule.php:1007
ResourceLoaderModule\saveFileDependencies
saveFileDependencies(ResourceLoaderContext $context, array $curFileRefs)
Save the indirect dependencies for this module persuant to the skin/language context.
Definition: ResourceLoaderModule.php:506
$wgStylePath
$wgStylePath
The URL path of the skins directory.
Definition: DefaultSettings.php:207
ResourceLoaderFileModule\getDefinitionSummary
getDefinitionSummary(ResourceLoaderContext $context)
Get the definition summary for this module.
Definition: ResourceLoaderFileModule.php:591
FileContentsHasher\getFileContentsHash
static getFileContentsHash( $filePaths, $algo='md4')
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
Definition: FileContentsHasher.php:88
Skin\getSkinNames
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:61
ResourceLoaderModule\getLessVars
getLessVars(ResourceLoaderContext $context)
Get module-specific LESS variables, if any.
Definition: ResourceLoaderModule.php:689
ResourceLoaderFileModule\$vueComponentParser
VueComponentParser $vueComponentParser
Lazy-created by getVueComponentParser()
Definition: ResourceLoaderFileModule.php:183
ResourceLoaderFileModule
Module based on local JavaScript/CSS files.
Definition: ResourceLoaderFileModule.php:39
ResourceLoaderFileModule\getGroup
getGroup()
Gets the name of the group this module should be loaded in.
Definition: ResourceLoaderFileModule.php:460
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1030
ResourceLoaderModule\getLogger
getLogger()
Definition: ResourceLoaderModule.php:251
ResourceLoaderFileModule\getSkinStyleFiles
getSkinStyleFiles( $skinName)
Gets a list of file paths for all skin styles in the module used by the skin.
Definition: ResourceLoaderFileModule.php:838
ResourceLoaderFileModule\compileLessFile
compileLessFile( $fileName, ResourceLoaderContext $context)
Definition: ResourceLoaderFileModule.php:1049
ResourceLoaderFileModule\getAllStyleFiles
getAllStyleFiles()
Returns all style files and all skin style files used by this module.
Definition: ResourceLoaderFileModule.php:872
ResourceLoaderFileModule\processStyle
processStyle( $style, $styleLang, $path, ResourceLoaderContext $context)
Process a CSS/LESS string.
Definition: ResourceLoaderFileModule.php:969
ResourceLoaderFileModule\getStyleURLsForDebug
getStyleURLsForDebug(ResourceLoaderContext $context)
Definition: ResourceLoaderFileModule.php:425
ResourceLoaderFileModule\getRemotePath
getRemotePath( $path)
Definition: ResourceLoaderFileModule.php:685
ResourceLoaderModule\getDeprecationInformation
getDeprecationInformation(ResourceLoaderContext $context)
Get JS representing deprecation information for the current module if available.
Definition: ResourceLoaderModule.php:170
ResourceLoaderFileModule\$targets
$targets
Definition: ResourceLoaderFileModule.php:154
ResourceLoaderFileModule\expandPackageFiles
expandPackageFiles(ResourceLoaderContext $context)
Internal helper for use by getPackageFiles(), getFileHashes() and getDefinitionSummary().
Definition: ResourceLoaderFileModule.php:1149
ResourceLoaderContext\getLanguage
getLanguage()
Definition: ResourceLoaderContext.php:154
ResourceLoaderFileModule\$messages
array $messages
List of message keys used by this module.
Definition: ResourceLoaderFileModule.php:146
ResourceLoaderModule\getMessageBlob
getMessageBlob(ResourceLoaderContext $context)
Get the hash of the message blob.
Definition: ResourceLoaderModule.php:577
ResourceLoaderFileModule\$languageScripts
array $languageScripts
List of JavaScript files to include when using a specific language.
Definition: ResourceLoaderFileModule.php:66
ResourceLoaderFileModule\stripBom
stripBom( $input)
Takes an input string and removes the UTF-8 BOM character if present.
Definition: ResourceLoaderFileModule.php:1379
ResourceLoaderFileModule\extractBasePaths
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information.
Definition: ResourceLoaderFileModule.php:298
$urls
$urls
Definition: opensearch_desc.php:82
ResourceLoaderFileModule\getPackageFiles
getPackageFiles(ResourceLoaderContext $context)
Resolves the package files defintion and generates the content of each package file.
Definition: ResourceLoaderFileModule.php:1286
$content
$content
Definition: router.php:76
ResourceLoaderFileModule\getDependencies
getDependencies(ResourceLoaderContext $context=null)
Gets list of names of modules this module depends on.
Definition: ResourceLoaderFileModule.php:469
CSSMin\getLocalFileReferences
static getLocalFileReferences( $source, $path)
Get a list of local files referenced in a stylesheet (includes non-existent files).
Definition: CSSMin.php:63
ResourceLoaderFileModule\getTargets
getTargets()
Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile'].
Definition: ResourceLoaderFileModule.php:1016
ResourceLoaderFileModule\$skipFunction
string $skipFunction
File name containing the body of the skip function.
Definition: ResourceLoaderFileModule.php:137
ResourceLoaderFileModule\$group
string $group
Name of group to load this module in.
Definition: ResourceLoaderFileModule.php:149
ResourceLoaderFileModule\$localFileRefs
array $localFileRefs
Place where readStyleFile() tracks file dependencies.
Definition: ResourceLoaderFileModule.php:172
CACHE_ANYTHING
const CACHE_ANYTHING
Definition: Defines.php:90
ResourceLoaderFileModule\readScriptFiles
readScriptFiles(array $scripts)
Get the contents of a list of JavaScript files.
Definition: ResourceLoaderFileModule.php:896
ResourceLoaderFileModule\getPath
getPath( $path)
Definition: ResourceLoaderFileModule.php:661
ResourceLoaderFileModule\collateFilePathListByOption
static collateFilePathListByOption(array $list, $option, $default)
Collates file paths by option (where provided).
Definition: ResourceLoaderFileModule.php:728
VueComponentParser
Parser for Vue single file components (.vue files).
Definition: VueComponentParser.php:28
ResourceLoaderFileModule\$expandedPackageFiles
array $expandedPackageFiles
Expanded versions of $packageFiles, lazy-computed by expandPackageFiles(); keyed by context hash.
Definition: ResourceLoaderFileModule.php:117
ResourceLoaderFileModule\$localBasePath
string $localBasePath
Local base path, see __construct()
Definition: ResourceLoaderFileModule.php:42
$wgResourceBasePath
$wgResourceBasePath
The default 'remoteBasePath' value for instances of ResourceLoaderFileModule.
Definition: DefaultSettings.php:4056
$context
$context
Definition: load.php:43
ResourceLoaderFileModule\getScriptFiles
getScriptFiles(ResourceLoaderContext $context)
Get a list of script file paths for this module, in order of proper execution.
Definition: ResourceLoaderFileModule.php:776
ResourceLoaderFileModule\getStyleSheetLang
getStyleSheetLang( $path)
Infer the stylesheet language from a stylesheet file path.
Definition: ResourceLoaderFileModule.php:700
ResourceLoaderFileModule\getLocalPath
getLocalPath( $path)
Definition: ResourceLoaderFileModule.php:673
ResourceLoaderModule
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
Definition: ResourceLoaderModule.php:38
$cache
$cache
Definition: mcc.php:33
ResourceLoaderFileModule\tryForKey
static tryForKey(array $list, $key, $fallback=null)
Get a list of element that match a key, optionally using a fallback key.
Definition: ResourceLoaderFileModule.php:758
ResourceLoaderFileModule\readStyleFile
readStyleFile( $path, ResourceLoaderContext $context)
Read and process a style file.
Definition: ResourceLoaderFileModule.php:945
ResourceLoaderFileModule\getTemplates
getTemplates()
Takes named templates by the module and returns an array mapping.
Definition: ResourceLoaderFileModule.php:1114
ResourceLoaderFileModule\$fullyExpandedPackageFiles
array $fullyExpandedPackageFiles
Further expanded versions of $expandedPackageFiles, lazy-computed by getPackageFiles(); keyed by cont...
Definition: ResourceLoaderFileModule.php:123
$path
$path
Definition: NoLocalSettings.php:25
ResourceLoaderFileModule\readStyleFiles
readStyleFiles(array $styles, ResourceLoaderContext $context)
Get the contents of a list of CSS files.
Definition: ResourceLoaderFileModule.php:919
ResourceLoaderModule\getFileDependencies
getFileDependencies(ResourceLoaderContext $context)
Get the indirect dependencies for this module persuant to the skin/language context.
Definition: ResourceLoaderModule.php:467
ResourceLoaderFileModule\$remoteBasePath
string $remoteBasePath
Remote base path, see __construct()
Definition: ResourceLoaderFileModule.php:45
ResourceLoaderFileModule\supportsURLLoading
supportsURLLoading()
Definition: ResourceLoaderFileModule.php:381
ResourceLoaderFileModule\getFileContents
getFileContents( $localPath, $type)
Helper method for getting a file.
Definition: ResourceLoaderFileModule.php:481
ResourceLoaderFileModule\getMessages
getMessages()
Gets list of message keys used by this module.
Definition: ResourceLoaderFileModule.php:451
ResourceLoaderFileModule\getVueComponentParser
getVueComponentParser()
Definition: ResourceLoaderFileModule.php:650
$IP
$IP
Definition: WebStart.php:49
ResourceLoaderFileModule\getAllSkinStyleFiles
getAllSkinStyleFiles()
Gets a list of file paths for all skin style files in the module, for all available skins.
Definition: ResourceLoaderFileModule.php:852
ResourceLoaderFileModule\getStyleFiles
getStyleFiles(ResourceLoaderContext $context)
Get a list of file paths for all styles in this module, in order of proper inclusion.
Definition: ResourceLoaderFileModule.php:820
ResourceLoaderFileModule\getType
getType()
Get the module's load type.
Definition: ResourceLoaderFileModule.php:1026
ResourceLoaderFileModule\getScript
getScript(ResourceLoaderContext $context)
Gets all scripts for a given context concatenated together.
Definition: ResourceLoaderFileModule.php:342
ResourceLoaderFileModule\$scripts
array $scripts
List of paths to JavaScript files to always include.
Definition: ResourceLoaderFileModule.php:57
ResourceLoaderFileModule\getLanguageScripts
getLanguageScripts( $lang)
Get the set of language scripts for the given language, possibly using a fallback language.
Definition: ResourceLoaderFileModule.php:796
ResourceLoaderModule\getConfig
getConfig()
Definition: ResourceLoaderModule.php:222
ResourceLoaderFileModule\$missingLocalFileRefs
array $missingLocalFileRefs
Place where readStyleFile() tracks file dependencies for non-existent files.
Definition: ResourceLoaderFileModule.php:178
OutputPage\transformResourcePath
static transformResourcePath(Config $config, $path)
Transform path to web-accessible static resource.
Definition: OutputPage.php:3899
ResourceLoaderFileModule\$packageFiles
array $packageFiles
List of packaged files to make available through require()
Definition: ResourceLoaderFileModule.php:111
ObjectCache\getLocalServerInstance
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
Definition: ObjectCache.php:254
ResourceLoaderFileModule\getPackageFileType
static getPackageFileType( $path)
Infer the file type from a package file path.
Definition: ResourceLoaderFileModule.php:709
$type
$type
Definition: testCompression.php:52