MediaWiki  master
ResourceLoaderFileModule.php
Go to the documentation of this file.
1 <?php
36 
38  protected $localBasePath = '';
39 
41  protected $remoteBasePath = '';
42 
44  protected $templates = [];
45 
53  protected $scripts = [];
54 
62  protected $languageScripts = [];
63 
71  protected $skinScripts = [];
72 
80  protected $debugScripts = [];
81 
89  protected $styles = [];
90 
98  protected $skinStyles = [];
99 
107  protected $packageFiles = null;
108 
113  private $expandedPackageFiles = [];
114 
122  protected $dependencies = [];
123 
127  protected $skipFunction = null;
128 
136  protected $messages = [];
137 
139  protected $group;
140 
142  protected $debugRaw = true;
143 
144  protected $targets = [ 'desktop' ];
145 
147  protected $noflip = false;
148 
153  protected $hasGeneratedStyles = false;
154 
162  protected $localFileRefs = [];
163 
168  protected $missingLocalFileRefs = [];
169 
240  public function __construct(
241  array $options = [],
242  $localBasePath = null,
243  $remoteBasePath = null
244  ) {
245  // Flag to decide whether to automagically add the mediawiki.template module
246  $hasTemplates = false;
247  // localBasePath and remoteBasePath both have unbelievably long fallback chains
248  // and need to be handled separately.
249  list( $this->localBasePath, $this->remoteBasePath ) =
250  self::extractBasePaths( $options, $localBasePath, $remoteBasePath );
251 
252  // Extract, validate and normalise remaining options
253  foreach ( $options as $member => $option ) {
254  switch ( $member ) {
255  // Lists of file paths
256  case 'scripts':
257  case 'debugScripts':
258  case 'styles':
259  case 'packageFiles':
260  $this->{$member} = is_array( $option ) ? $option : [ $option ];
261  break;
262  case 'templates':
263  $hasTemplates = true;
264  $this->{$member} = is_array( $option ) ? $option : [ $option ];
265  break;
266  // Collated lists of file paths
267  case 'languageScripts':
268  case 'skinScripts':
269  case 'skinStyles':
270  if ( !is_array( $option ) ) {
271  throw new InvalidArgumentException(
272  "Invalid collated file path list error. " .
273  "'$option' given, array expected."
274  );
275  }
276  foreach ( $option as $key => $value ) {
277  if ( !is_string( $key ) ) {
278  throw new InvalidArgumentException(
279  "Invalid collated file path list key error. " .
280  "'$key' given, string expected."
281  );
282  }
283  $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
284  }
285  break;
286  case 'deprecated':
287  $this->deprecated = $option;
288  break;
289  // Lists of strings
290  case 'dependencies':
291  case 'messages':
292  case 'targets':
293  // Normalise
294  $option = array_values( array_unique( (array)$option ) );
295  sort( $option );
296 
297  $this->{$member} = $option;
298  break;
299  // Single strings
300  case 'group':
301  case 'skipFunction':
302  $this->{$member} = (string)$option;
303  break;
304  // Single booleans
305  case 'debugRaw':
306  case 'noflip':
307  $this->{$member} = (bool)$option;
308  break;
309  }
310  }
311  if ( isset( $options['scripts'] ) && isset( $options['packageFiles'] ) ) {
312  throw new InvalidArgumentException( "A module may not set both 'scripts' and 'packageFiles'" );
313  }
314  if ( $hasTemplates ) {
315  $this->dependencies[] = 'mediawiki.template';
316  // Ensure relevant template compiler module gets loaded
317  foreach ( $this->templates as $alias => $templatePath ) {
318  if ( is_int( $alias ) ) {
319  $alias = $this->getPath( $templatePath );
320  }
321  $suffix = explode( '.', $alias );
322  $suffix = end( $suffix );
323  $compilerModule = 'mediawiki.template.' . $suffix;
324  if ( $suffix !== 'html' && !in_array( $compilerModule, $this->dependencies ) ) {
325  $this->dependencies[] = $compilerModule;
326  }
327  }
328  }
329  }
330 
342  public static function extractBasePaths(
343  array $options = [],
344  $localBasePath = null,
345  $remoteBasePath = null
346  ) {
347  global $IP, $wgResourceBasePath;
348 
349  // The different ways these checks are done, and their ordering, look very silly,
350  // but were preserved for backwards-compatibility just in case. Tread lightly.
351 
352  if ( $localBasePath === null ) {
354  }
355  if ( $remoteBasePath === null ) {
357  }
358 
359  if ( isset( $options['remoteExtPath'] ) ) {
360  global $wgExtensionAssetsPath;
361  $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
362  }
363 
364  if ( isset( $options['remoteSkinPath'] ) ) {
365  global $wgStylePath;
366  $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
367  }
368 
369  if ( array_key_exists( 'localBasePath', $options ) ) {
370  $localBasePath = (string)$options['localBasePath'];
371  }
372 
373  if ( array_key_exists( 'remoteBasePath', $options ) ) {
374  $remoteBasePath = (string)$options['remoteBasePath'];
375  }
376 
377  return [ $localBasePath, $remoteBasePath ];
378  }
379 
387  $deprecationScript = $this->getDeprecationInformation( $context );
388  if ( $this->packageFiles !== null ) {
389  $packageFiles = $this->getPackageFiles( $context );
390  if ( $deprecationScript ) {
391  $mainFile =& $packageFiles['files'][$packageFiles['main']];
392  $mainFile['content'] = $deprecationScript . $mainFile['content'];
393  }
394  return $packageFiles;
395  }
396 
397  $files = $this->getScriptFiles( $context );
398  return $deprecationScript . $this->readScriptFiles( $files );
399  }
400 
406  $urls = [];
407  foreach ( $this->getScriptFiles( $context ) as $file ) {
409  $this->getConfig(),
410  $this->getRemotePath( $file )
411  );
412  }
413  return $urls;
414  }
415 
419  public function supportsURLLoading() {
420  // If package files are involved, don't support URL loading, because that breaks
421  // scoped require() functions
422  return $this->debugRaw && !$this->packageFiles;
423  }
424 
432  $styles = $this->readStyleFiles(
433  $this->getStyleFiles( $context ),
434  $this->getFlip( $context ),
435  $context
436  );
437  // Collect referenced files
438  $this->saveFileDependencies( $context, $this->localFileRefs );
439 
440  return $styles;
441  }
442 
448  if ( $this->hasGeneratedStyles ) {
449  // Do the default behaviour of returning a url back to load.php
450  // but with only=styles.
451  return parent::getStyleURLsForDebug( $context );
452  }
453  // Our module consists entirely of real css files,
454  // in debug mode we can load those directly.
455  $urls = [];
456  foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
457  $urls[$mediaType] = [];
458  foreach ( $list as $file ) {
460  $this->getConfig(),
461  $this->getRemotePath( $file )
462  );
463  }
464  }
465  return $urls;
466  }
467 
473  public function getMessages() {
474  return $this->messages;
475  }
476 
482  public function getGroup() {
483  return $this->group;
484  }
485 
491  public function getDependencies( ResourceLoaderContext $context = null ) {
492  return $this->dependencies;
493  }
494 
500  public function getSkipFunction() {
501  if ( !$this->skipFunction ) {
502  return null;
503  }
504 
505  $localPath = $this->getLocalPath( $this->skipFunction );
506  if ( !file_exists( $localPath ) ) {
507  throw new MWException( __METHOD__ . ": skip function file not found: \"$localPath\"" );
508  }
509  $contents = $this->stripBom( file_get_contents( $localPath ) );
510  return $contents;
511  }
512 
521  public function enableModuleContentVersion() {
522  return false;
523  }
524 
533  $files = [];
534 
535  // Flatten style files into $files
536  $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' );
537  foreach ( $styles as $styleFiles ) {
538  $files = array_merge( $files, $styleFiles );
539  }
540 
541  $skinFiles = self::collateFilePathListByOption(
542  self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
543  'media',
544  'all'
545  );
546  foreach ( $skinFiles as $styleFiles ) {
547  $files = array_merge( $files, $styleFiles );
548  }
549 
550  // Extract file names for package files
551  $expandedPackageFiles = $this->expandPackageFiles( $context );
553  array_filter( array_map( function ( $fileInfo ) {
554  return $fileInfo['filePath'] ?? null;
555  }, $expandedPackageFiles['files'] ) ) :
556  [];
557 
558  // Final merge, this should result in a master list of dependent files
559  $files = array_merge(
560  $files,
562  $this->scripts,
563  $this->templates,
564  $context->getDebug() ? $this->debugScripts : [],
565  $this->getLanguageScripts( $context->getLanguage() ),
566  self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
567  );
568  if ( $this->skipFunction ) {
569  $files[] = $this->skipFunction;
570  }
571  $files = array_map( [ $this, 'getLocalPath' ], $files );
572  // File deps need to be treated separately because they're already prefixed
573  $files = array_merge( $files, $this->getFileDependencies( $context ) );
574  // Filter out any duplicates from getFileDependencies() and others.
575  // Most commonly introduced by compileLessFile(), which always includes the
576  // entry point Less file we already know about.
577  $files = array_values( array_unique( $files ) );
578 
579  // Don't include keys or file paths here, only the hashes. Including that would needlessly
580  // cause global cache invalidation when files move or if e.g. the MediaWiki path changes.
581  // Any significant ordering is already detected by the definition summary.
582  return array_map( [ __CLASS__, 'safeFileHash' ], $files );
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, simplied 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 this already.
625  // It is important that the keys of the 'files' array are preserved,
626  // as they 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 
651  protected function getPath( $path ) {
652  if ( $path instanceof ResourceLoaderFilePath ) {
653  return $path->getPath();
654  }
655 
656  return $path;
657  }
658 
663  protected function getLocalPath( $path ) {
664  if ( $path instanceof ResourceLoaderFilePath ) {
665  return $path->getLocalPath();
666  }
667 
668  return "{$this->localBasePath}/$path";
669  }
670 
675  protected function getRemotePath( $path ) {
676  if ( $path instanceof ResourceLoaderFilePath ) {
677  return $path->getRemotePath();
678  }
679 
680  return "{$this->remoteBasePath}/$path";
681  }
682 
690  public function getStyleSheetLang( $path ) {
691  return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
692  }
693 
699  public static function getPackageFileType( $path ) {
700  if ( preg_match( '/\.json$/i', $path ) ) {
701  return 'data';
702  }
703  return 'script';
704  }
705 
715  protected static function collateFilePathListByOption( array $list, $option, $default ) {
716  $collatedFiles = [];
717  foreach ( (array)$list as $key => $value ) {
718  if ( is_int( $key ) ) {
719  // File name as the value
720  if ( !isset( $collatedFiles[$default] ) ) {
721  $collatedFiles[$default] = [];
722  }
723  $collatedFiles[$default][] = $value;
724  } elseif ( is_array( $value ) ) {
725  // File name as the key, options array as the value
726  $optionValue = $value[$option] ?? $default;
727  if ( !isset( $collatedFiles[$optionValue] ) ) {
728  $collatedFiles[$optionValue] = [];
729  }
730  $collatedFiles[$optionValue][] = $key;
731  }
732  }
733  return $collatedFiles;
734  }
735 
745  protected static function tryForKey( array $list, $key, $fallback = null ) {
746  if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
747  return $list[$key];
748  } elseif ( is_string( $fallback )
749  && isset( $list[$fallback] )
750  && is_array( $list[$fallback] )
751  ) {
752  return $list[$fallback];
753  }
754  return [];
755  }
756 
764  $files = array_merge(
765  $this->scripts,
766  $this->getLanguageScripts( $context->getLanguage() ),
767  self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
768  );
769  if ( $context->getDebug() ) {
770  $files = array_merge( $files, $this->debugScripts );
771  }
772 
773  return array_unique( $files, SORT_REGULAR );
774  }
775 
783  private function getLanguageScripts( $lang ) {
784  $scripts = self::tryForKey( $this->languageScripts, $lang );
785  if ( $scripts ) {
786  return $scripts;
787  }
788  $fallbacks = Language::getFallbacksFor( $lang );
789  foreach ( $fallbacks as $lang ) {
790  $scripts = self::tryForKey( $this->languageScripts, $lang );
791  if ( $scripts ) {
792  return $scripts;
793  }
794  }
795 
796  return [];
797  }
798 
807  return array_merge_recursive(
808  self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
809  self::collateFilePathListByOption(
810  self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
811  'media',
812  'all'
813  )
814  );
815  }
816 
824  protected function getSkinStyleFiles( $skinName ) {
825  return self::collateFilePathListByOption(
826  self::tryForKey( $this->skinStyles, $skinName ),
827  'media',
828  'all'
829  );
830  }
831 
838  protected function getAllSkinStyleFiles() {
839  $styleFiles = [];
840  $internalSkinNames = array_keys( Skin::getSkinNames() );
841  $internalSkinNames[] = 'default';
842 
843  foreach ( $internalSkinNames as $internalSkinName ) {
844  $styleFiles = array_merge_recursive(
845  $styleFiles,
846  $this->getSkinStyleFiles( $internalSkinName )
847  );
848  }
849 
850  return $styleFiles;
851  }
852 
858  public function getAllStyleFiles() {
859  $collatedStyleFiles = array_merge_recursive(
860  self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
861  $this->getAllSkinStyleFiles()
862  );
863 
864  $result = [];
865 
866  foreach ( $collatedStyleFiles as $media => $styleFiles ) {
867  foreach ( $styleFiles as $styleFile ) {
868  $result[] = $this->getLocalPath( $styleFile );
869  }
870  }
871 
872  return $result;
873  }
874 
882  private function readScriptFiles( array $scripts ) {
883  if ( empty( $scripts ) ) {
884  return '';
885  }
886  $js = '';
887  foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
888  $localPath = $this->getLocalPath( $fileName );
889  if ( !file_exists( $localPath ) ) {
890  throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" );
891  }
892  $contents = $this->stripBom( file_get_contents( $localPath ) );
893  $js .= $contents . "\n";
894  }
895  return $js;
896  }
897 
909  public function readStyleFiles( array $styles, $flip, ResourceLoaderContext $context ) {
910  if ( !$styles ) {
911  return [];
912  }
913  foreach ( $styles as $media => $files ) {
914  $uniqueFiles = array_unique( $files, SORT_REGULAR );
915  $styleFiles = [];
916  foreach ( $uniqueFiles as $file ) {
917  $styleFiles[] = $this->readStyleFile( $file, $flip, $context );
918  }
919  $styles[$media] = implode( "\n", $styleFiles );
920  }
921  return $styles;
922  }
923 
936  protected function readStyleFile( $path, $flip, ResourceLoaderContext $context ) {
937  $localPath = $this->getLocalPath( $path );
938  $remotePath = $this->getRemotePath( $path );
939  if ( !file_exists( $localPath ) ) {
940  $msg = __METHOD__ . ": style file not found: \"$localPath\"";
941  wfDebugLog( 'resourceloader', $msg );
942  throw new MWException( $msg );
943  }
944 
945  if ( $this->getStyleSheetLang( $localPath ) === 'less' ) {
946  $style = $this->compileLessFile( $localPath, $context );
947  $this->hasGeneratedStyles = true;
948  } else {
949  $style = $this->stripBom( file_get_contents( $localPath ) );
950  }
951 
952  if ( $flip ) {
953  $style = CSSJanus::transform( $style, true, false );
954  }
955  $localDir = dirname( $localPath );
956  $remoteDir = dirname( $remotePath );
957  // Get and register local file references
958  $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
959  foreach ( $localFileRefs as $file ) {
960  if ( file_exists( $file ) ) {
961  $this->localFileRefs[] = $file;
962  } else {
963  $this->missingLocalFileRefs[] = $file;
964  }
965  }
966  // Don't cache this call. remap() ensures data URIs embeds are up to date,
967  // and urls contain correct content hashes in their query string. (T128668)
968  return CSSMin::remap( $style, $localDir, $remoteDir, true );
969  }
970 
977  return $context->getDirection() === 'rtl' && !$this->noflip;
978  }
979 
985  public function getTargets() {
986  return $this->targets;
987  }
988 
995  public function getType() {
996  $canBeStylesOnly = !(
997  // All options except 'styles', 'skinStyles' and 'debugRaw'
998  $this->scripts
999  || $this->debugScripts
1000  || $this->templates
1001  || $this->languageScripts
1002  || $this->skinScripts
1003  || $this->dependencies
1004  || $this->messages
1005  || $this->skipFunction
1007  );
1008  return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
1009  }
1010 
1023  protected function compileLessFile( $fileName, ResourceLoaderContext $context ) {
1024  static $cache;
1025 
1026  if ( !$cache ) {
1028  }
1029 
1030  $vars = $this->getLessVars( $context );
1031  // Construct a cache key from the LESS file name, and a hash digest
1032  // of the LESS variables used for compilation.
1033  ksort( $vars );
1034  $varsHash = hash( 'md4', serialize( $vars ) );
1035  $cacheKey = $cache->makeGlobalKey( 'LESS', $fileName, $varsHash );
1036  $cachedCompile = $cache->get( $cacheKey );
1037 
1038  // If we got a cached value, we have to validate it by getting a
1039  // checksum of all the files that were loaded by the parser and
1040  // ensuring it matches the cached entry's.
1041  if ( isset( $cachedCompile['hash'] ) ) {
1042  $contentHash = FileContentsHasher::getFileContentsHash( $cachedCompile['files'] );
1043  if ( $contentHash === $cachedCompile['hash'] ) {
1044  $this->localFileRefs = array_merge( $this->localFileRefs, $cachedCompile['files'] );
1045  return $cachedCompile['css'];
1046  }
1047  }
1048 
1049  $compiler = $context->getResourceLoader()->getLessCompiler( $vars );
1050  $css = $compiler->parseFile( $fileName )->getCss();
1051  $files = $compiler->AllParsedFiles();
1052  $this->localFileRefs = array_merge( $this->localFileRefs, $files );
1053 
1054  // Cache for 24 hours (86400 seconds).
1055  $cache->set( $cacheKey, [
1056  'css' => $css,
1057  'files' => $files,
1058  'hash' => FileContentsHasher::getFileContentsHash( $files ),
1059  ], 3600 * 24 );
1060 
1061  return $css;
1062  }
1063 
1069  public function getTemplates() {
1070  $templates = [];
1071 
1072  foreach ( $this->templates as $alias => $templatePath ) {
1073  // Alias is optional
1074  if ( is_int( $alias ) ) {
1075  $alias = $this->getPath( $templatePath );
1076  }
1077  $localPath = $this->getLocalPath( $templatePath );
1078  if ( file_exists( $localPath ) ) {
1079  $content = file_get_contents( $localPath );
1080  $templates[$alias] = $this->stripBom( $content );
1081  } else {
1082  $msg = __METHOD__ . ": template file not found: \"$localPath\"";
1083  wfDebugLog( 'resourceloader', $msg );
1084  throw new MWException( $msg );
1085  }
1086  }
1087  return $templates;
1088  }
1089 
1109  $hash = $context->getHash();
1110  if ( isset( $this->expandedPackageFiles[$hash] ) ) {
1111  return $this->expandedPackageFiles[$hash];
1112  }
1113  if ( $this->packageFiles === null ) {
1114  return null;
1115  }
1116  $expandedFiles = [];
1117  $mainFile = null;
1118 
1119  foreach ( $this->packageFiles as $alias => $fileInfo ) {
1120  if ( is_string( $fileInfo ) ) {
1121  $fileInfo = [ 'name' => $fileInfo, 'file' => $fileInfo ];
1122  } elseif ( !isset( $fileInfo['name'] ) ) {
1123  $msg = __METHOD__ . ": invalid package file definition for module " .
1124  "\"{$this->getName()}\": 'name' key is required when value is not a string";
1125  wfDebugLog( 'resourceloader', $msg );
1126  throw new MWException( $msg );
1127  }
1128 
1129  // Infer type from alias if needed
1130  $type = $fileInfo['type'] ?? self::getPackageFileType( $fileInfo['name'] );
1131  $expanded = [ 'type' => $type ];
1132  if ( !empty( $fileInfo['main'] ) ) {
1133  $mainFile = $fileInfo['name'];
1134  if ( $type !== 'script' ) {
1135  $msg = __METHOD__ . ": invalid package file definition for module " .
1136  "\"{$this->getName()}\": main file \"$mainFile\" must be of type \"script\", not \"$type\"";
1137  wfDebugLog( 'resourceloader', $msg );
1138  throw new MWException( $msg );
1139  }
1140  }
1141 
1142  // Perform expansions (except 'file' and 'callback'), creating one of these keys:
1143  // - 'content': literal value.
1144  // - 'filePath': content to be read from a file.
1145  // - 'callback': content computed by a callable.
1146  if ( isset( $fileInfo['content'] ) ) {
1147  $expanded['content'] = $fileInfo['content'];
1148  } elseif ( isset( $fileInfo['file'] ) ) {
1149  $expanded['filePath'] = $fileInfo['file'];
1150  } elseif ( isset( $fileInfo['callback'] ) ) {
1151  if ( !is_callable( $fileInfo['callback'] ) ) {
1152  $msg = __METHOD__ . ": invalid callback for package file \"{$fileInfo['name']}\"" .
1153  " in module \"{$this->getName()}\"";
1154  wfDebugLog( 'resourceloader', $msg );
1155  throw new MWException( $msg );
1156  }
1157  if ( isset( $fileInfo['versionCallback'] ) ) {
1158  if ( !is_callable( $fileInfo['versionCallback'] ) ) {
1159  throw new MWException( __METHOD__ . ": invalid versionCallback for file" .
1160  " \"{$fileInfo['name']}\" in module \"{$this->getName()}\"" );
1161  }
1162  $expanded['definitionSummary'] =
1163  ( $fileInfo['versionCallback'] )( $context, $this->getConfig() );
1164  // Don't invoke 'callback' here as it may be expensive (T223260).
1165  $expanded['callback'] = $fileInfo['callback'];
1166  } else {
1167  $expanded['content'] = ( $fileInfo['callback'] )( $context, $this->getConfig() );
1168  }
1169  } elseif ( isset( $fileInfo['config'] ) ) {
1170  if ( $type !== 'data' ) {
1171  $msg = __METHOD__ . ": invalid use of \"config\" for package file \"{$fileInfo['name']}\" " .
1172  "in module \"{$this->getName()}\": type must be \"data\" but is \"$type\"";
1173  wfDebugLog( 'resourceloader', $msg );
1174  throw new MWException( $msg );
1175  }
1176  $expandedConfig = [];
1177  foreach ( $fileInfo['config'] as $key => $var ) {
1178  $expandedConfig[ is_numeric( $key ) ? $var : $key ] = $this->getConfig()->get( $var );
1179  }
1180  $expanded['content'] = $expandedConfig;
1181  } elseif ( !empty( $fileInfo['main'] ) ) {
1182  // [ 'name' => 'foo.js', 'main' => true ] is shorthand
1183  $expanded['filePath'] = $fileInfo['name'];
1184  } else {
1185  $msg = __METHOD__ . ": invalid package file definition for \"{$fileInfo['name']}\" " .
1186  "in module \"{$this->getName()}\": one of \"file\", \"content\", \"callback\" or \"config\" " .
1187  "must be set";
1188  wfDebugLog( 'resourceloader', $msg );
1189  throw new MWException( $msg );
1190  }
1191 
1192  $expandedFiles[$fileInfo['name']] = $expanded;
1193  }
1194 
1195  if ( $expandedFiles && $mainFile === null ) {
1196  // The first package file that is a script is the main file
1197  foreach ( $expandedFiles as $path => &$file ) {
1198  if ( $file['type'] === 'script' ) {
1199  $mainFile = $path;
1200  break;
1201  }
1202  }
1203  }
1204 
1205  $result = [
1206  'main' => $mainFile,
1207  'files' => $expandedFiles
1208  ];
1209 
1210  $this->expandedPackageFiles[$hash] = $result;
1211  return $result;
1212  }
1213 
1220  if ( $this->packageFiles === null ) {
1221  return null;
1222  }
1223  $expandedPackageFiles = $this->expandPackageFiles( $context );
1224 
1225  // Expand file contents
1226  foreach ( $expandedPackageFiles['files'] as &$fileInfo ) {
1227  // Turn any 'filePath' or 'callback' key into actual 'content',
1228  // and remove the key after that.
1229  if ( isset( $fileInfo['filePath'] ) ) {
1230  $localPath = $this->getLocalPath( $fileInfo['filePath'] );
1231  if ( !file_exists( $localPath ) ) {
1232  $msg = __METHOD__ . ": package file not found: \"$localPath\"" .
1233  " in module \"{$this->getName()}\"";
1234  wfDebugLog( 'resourceloader', $msg );
1235  throw new MWException( $msg );
1236  }
1237  $content = $this->stripBom( file_get_contents( $localPath ) );
1238  if ( $fileInfo['type'] === 'data' ) {
1239  $content = json_decode( $content );
1240  }
1241  $fileInfo['content'] = $content;
1242  unset( $fileInfo['filePath'] );
1243  } elseif ( isset( $fileInfo['callback'] ) ) {
1244  $fileInfo['content'] = ( $fileInfo['callback'] )( $context, $this->getConfig() );
1245  unset( $fileInfo['callback'] );
1246  }
1247 
1248  // Not needed for client response, exists for getDefinitionSummary().
1249  unset( $fileInfo['definitionSummary'] );
1250  }
1251 
1252  return $expandedPackageFiles;
1253  }
1254 
1265  protected function stripBom( $input ) {
1266  if ( substr_compare( "\xef\xbb\xbf", $input, 0, 3 ) === 0 ) {
1267  return substr( $input, 3 );
1268  }
1269  return $input;
1270  }
1271 }
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
getScriptURLsForDebug(ResourceLoaderContext $context)
__construct(array $options=[], $localBasePath=null, $remoteBasePath=null)
Constructs a new module from an options array.
$context
Definition: load.php:45
Abstraction for ResourceLoader modules, with name registration and maxage functionality.
serialize()
array $dependencies
List of modules this module depends on.
$IP
Definition: WebStart.php:41
array $skinStyles
List of paths to CSS files to include when using specific skins.
static getFileContentsHash( $filePaths, $algo='md4')
Get a hash of the combined contents of one or more files, either by retrieving a previously-computed ...
getStyleFiles(ResourceLoaderContext $context)
Get a list of file paths for all styles in this module, in order of proper inclusion.
array $packageFiles
List of packaged files to make available through require()
getFlip(ResourceLoaderContext $context)
Get whether CSS for this module should be flipped.
static getPackageFileType( $path)
Infer the file type from a package file path.
if(!isset( $args[0])) $lang
getMessageBlob(ResourceLoaderContext $context)
Get the hash of the message blob.
static getSkinNames()
Fetch the set of available skins.
Definition: Skin.php:57
getAllStyleFiles()
Returns all style files and all skin style files used by this module.
getSkipFunction()
Get the skip function.
compileLessFile( $fileName, ResourceLoaderContext $context)
Compile a LESS file into CSS.
array $debugScripts
List of paths to JavaScript files to include in debug mode.
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:239
getStyles(ResourceLoaderContext $context)
Get all styles for a given context.
array $templates
Saves a list of the templates named by the modules.
getLessVars(ResourceLoaderContext $context)
Get module-specific LESS variables, if any.
stripBom( $input)
Takes an input string and removes the UTF-8 BOM character if present.
getFileHashes(ResourceLoaderContext $context)
Helper method for getDefinitionSummary.
bool $debugRaw
Link to raw files in debug mode.
$wgStylePath
The URL path of the skins directory.
getFileDependencies(ResourceLoaderContext $context)
Get the files this module depends on indirectly for a given skin.
enableModuleContentVersion()
Disable module content versioning.
string $skipFunction
File name containing the body of the skip function.
bool $hasGeneratedStyles
Whether getStyleURLsForDebug should return raw file paths, or return load.php urls.
array $contents
Map of (context hash => cached module content)
static transformResourcePath(Config $config, $path)
Transform path to web-accessible static resource.
getDependencies(ResourceLoaderContext $context=null)
Gets list of names of modules this module depends on.
Module based on local JavaScript/CSS files.
readScriptFiles(array $scripts)
Get the contents of a list of JavaScript files.
string $group
Name of group to load this module in.
saveFileDependencies(ResourceLoaderContext $context, array $localFileRefs)
Set the files this module depends on indirectly for a given skin.
static string [] static getLocalFileReferences( $source, $path)
Get a list of local files referenced in a stylesheet (includes non-existent files).
Definition: CSSMin.php:63
array $localFileRefs
Place where readStyleFile() tracks file dependencies.
string $localBasePath
Local base path, see __construct()
$cache
Definition: mcc.php:33
$wgResourceBasePath
The default &#39;remoteBasePath&#39; value for instances of ResourceLoaderFileModule.
getDefinitionSummary(ResourceLoaderContext $context)
Get the definition summary for this module.
getStyleSheetLang( $path)
Infer the stylesheet language from a stylesheet file path.
readStyleFile( $path, $flip, ResourceLoaderContext $context)
Reads a style file.
getSkinStyleFiles( $skinName)
Gets a list of file paths for all skin styles in the module used by the skin.
readStyleFiles(array $styles, $flip, ResourceLoaderContext $context)
Get the contents of a list of CSS files.
getTemplates()
Takes named templates by the module and returns an array mapping.
getGroup()
Gets the name of the group this module should be loaded in.
An object to represent a path to a JavaScript/CSS file, along with a remote and local base path...
getMessages()
Gets list of message keys used by this module.
getStyleURLsForDebug(ResourceLoaderContext $context)
string $remoteBasePath
Remote base path, see __construct()
$fallback
Definition: MessagesAb.php:11
array $messages
List of message keys used by this module.
getAllSkinStyleFiles()
Gets a list of file paths for all skin style files in the module, for all available skins...
expandPackageFiles(ResourceLoaderContext $context)
Internal helper for use by getPackageFiles(), getFileHashes() and getDefinitionSummary().
array $scripts
List of paths to JavaScript files to always include.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
$wgExtensionAssetsPath
The URL path of the extensions directory.
static getFallbacksFor( $code, $mode=self::MESSAGES_FALLBACKS)
Get the ordered list of fallback languages.
Definition: Language.php:4386
array $languageScripts
List of JavaScript files to include when using a specific language.
getTargets()
Get target(s) for the module, eg [&#39;desktop&#39;] or [&#39;desktop&#39;, &#39;mobile&#39;].
const CACHE_ANYTHING
Definition: Defines.php:81
getType()
Get the module&#39;s load type.
static extractBasePaths(array $options=[], $localBasePath=null, $remoteBasePath=null)
Extract a pair of local and remote base paths from module definition information. ...
getScript(ResourceLoaderContext $context)
Gets all scripts for a given context concatenated together.
getPackageFiles(ResourceLoaderContext $context)
Resolves the package files defintion and generates the content of each package file.
getLanguageScripts( $lang)
Get the set of language scripts for the given language, possibly using a fallback language...
array $missingLocalFileRefs
Place where readStyleFile() tracks file dependencies for non-existent files.
static getLocalServerInstance( $fallback=CACHE_NONE)
Factory function for CACHE_ACCEL (referenced from DefaultSettings.php)
getHash()
All factors that uniquely identify this request, except &#39;modules&#39;.
$content
Definition: router.php:78
array $styles
List of paths to CSS files to always include.
static collateFilePathListByOption(array $list, $option, $default)
Collates file paths by option (where provided).
array $skinScripts
List of JavaScript files to include when using a specific skin.
getScriptFiles(ResourceLoaderContext $context)
Get a list of script file paths for this module, in order of proper execution.
getDeprecationInformation(ResourceLoaderContext $context)
Get JS representing deprecation information for the current module if available.
array $expandedPackageFiles
Expanded versions of $packageFiles, lazy-computed by expandPackageFiles(); keyed by context hash...
bool $noflip
Whether CSSJanus flipping should be skipped for this module.
Context object that contains information about the state of a specific ResourceLoader web request...
static tryForKey(array $list, $key, $fallback=null)
Get a list of element that match a key, optionally using a fallback key.