+ /**
+ * Internal helper for use by getPackageFiles(), getFileHashes() and getDefinitionSummary().
+ *
+ * This expands the 'packageFiles' definition into something that's (almost) the right format
+ * for getPackageFiles() to return. It expands shorthands, resolves config vars, and handles
+ * summarising any non-file data for getVersionHash(). For file-based data, getFileHashes()
+ * handles it instead, which also ends up in getDefinitionSummary().
+ *
+ * What it does not do is reading the actual contents of any specified files, nor invoking
+ * the computation callbacks. Those things are done by getPackageFiles() instead to improve
+ * backend performance by only doing this work when the module response is needed, and not
+ * when merely computing the version hash for StartupModule, or when checking
+ * If-None-Match headers for a HTTP 304 response.
+ *
+ * @param ResourceLoaderContext $context
+ * @return array|null
+ * @throws MWException If the 'packageFiles' definition is invalid.
+ */
+ private function expandPackageFiles( ResourceLoaderContext $context ) {
+ $hash = $context->getHash();
+ if ( isset( $this->expandedPackageFiles[$hash] ) ) {
+ return $this->expandedPackageFiles[$hash];
+ }
+ if ( $this->packageFiles === null ) {
+ return null;
+ }
+ $expandedFiles = [];
+ $mainFile = null;
+
+ foreach ( $this->packageFiles as $alias => $fileInfo ) {
+ if ( is_string( $fileInfo ) ) {
+ $fileInfo = [ 'name' => $fileInfo, 'file' => $fileInfo ];
+ } elseif ( !isset( $fileInfo['name'] ) ) {
+ $msg = __METHOD__ . ": invalid package file definition for module " .
+ "\"{$this->getName()}\": 'name' key is required when value is not a string";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+
+ // Infer type from alias if needed
+ $type = $fileInfo['type'] ?? self::getPackageFileType( $fileInfo['name'] );
+ $expanded = [ 'type' => $type ];
+ if ( !empty( $fileInfo['main'] ) ) {
+ $mainFile = $fileInfo['name'];
+ if ( $type !== 'script' ) {
+ $msg = __METHOD__ . ": invalid package file definition for module " .
+ "\"{$this->getName()}\": main file \"$mainFile\" must be of type \"script\", not \"$type\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+ }
+
+ // Perform expansions (except 'file' and 'callback'), creating one of these keys:
+ // - 'content': literal value.
+ // - 'filePath': content to be read from a file.
+ // - 'callback': content computed by a callable.
+ if ( isset( $fileInfo['content'] ) ) {
+ $expanded['content'] = $fileInfo['content'];
+ } elseif ( isset( $fileInfo['file'] ) ) {
+ $expanded['filePath'] = $fileInfo['file'];
+ } elseif ( isset( $fileInfo['callback'] ) ) {
+ if ( !is_callable( $fileInfo['callback'] ) ) {
+ $msg = __METHOD__ . ": invalid callback for package file \"{$fileInfo['name']}\"" .
+ " in module \"{$this->getName()}\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+ if ( isset( $fileInfo['versionCallback'] ) ) {
+ if ( !is_callable( $fileInfo['versionCallback'] ) ) {
+ throw new MWException( __METHOD__ . ": invalid versionCallback for file" .
+ " \"{$fileInfo['name']}\" in module \"{$this->getName()}\"" );
+ }
+ $expanded['definitionSummary'] = ( $fileInfo['versionCallback'] )( $context );
+ // Don't invoke 'callback' here as it may be expensive (T223260).
+ $expanded['callback'] = $fileInfo['callback'];
+ } else {
+ $expanded['content'] = ( $fileInfo['callback'] )( $context );
+ }
+ } elseif ( isset( $fileInfo['config'] ) ) {
+ if ( $type !== 'data' ) {
+ $msg = __METHOD__ . ": invalid use of \"config\" for package file \"{$fileInfo['name']}\" " .
+ "in module \"{$this->getName()}\": type must be \"data\" but is \"$type\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+ $expandedConfig = [];
+ foreach ( $fileInfo['config'] as $key => $var ) {
+ $expandedConfig[ is_numeric( $key ) ? $var : $key ] = $this->getConfig()->get( $var );
+ }
+ $expanded['content'] = $expandedConfig;
+ } elseif ( !empty( $fileInfo['main'] ) ) {
+ // [ 'name' => 'foo.js', 'main' => true ] is shorthand
+ $expanded['filePath'] = $fileInfo['name'];
+ } else {
+ $msg = __METHOD__ . ": invalid package file definition for \"{$fileInfo['name']}\" " .
+ "in module \"{$this->getName()}\": one of \"file\", \"content\", \"callback\" or \"config\" " .
+ "must be set";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+
+ $expandedFiles[$fileInfo['name']] = $expanded;
+ }
+
+ if ( $expandedFiles && $mainFile === null ) {
+ // The first package file that is a script is the main file
+ foreach ( $expandedFiles as $path => &$file ) {
+ if ( $file['type'] === 'script' ) {
+ $mainFile = $path;
+ break;
+ }
+ }
+ }
+
+ $result = [
+ 'main' => $mainFile,
+ 'files' => $expandedFiles
+ ];
+
+ $this->expandedPackageFiles[$hash] = $result;
+ return $result;
+ }
+
+ /**
+ * Resolves the package files defintion and generates the content of each package file.
+ * @param ResourceLoaderContext $context
+ * @return array Package files data structure, see ResourceLoaderModule::getScript()
+ */
+ public function getPackageFiles( ResourceLoaderContext $context ) {
+ if ( $this->packageFiles === null ) {
+ return null;
+ }
+ $expandedPackageFiles = $this->expandPackageFiles( $context );
+
+ // Expand file contents
+ foreach ( $expandedPackageFiles['files'] as &$fileInfo ) {
+ // Turn any 'filePath' or 'callback' key into actual 'content',
+ // and remove the key after that.
+ if ( isset( $fileInfo['filePath'] ) ) {
+ $localPath = $this->getLocalPath( $fileInfo['filePath'] );
+ if ( !file_exists( $localPath ) ) {
+ $msg = __METHOD__ . ": package file not found: \"$localPath\"" .
+ " in module \"{$this->getName()}\"";
+ wfDebugLog( 'resourceloader', $msg );
+ throw new MWException( $msg );
+ }
+ $content = $this->stripBom( file_get_contents( $localPath ) );
+ if ( $fileInfo['type'] === 'data' ) {
+ $content = json_decode( $content );
+ }
+ $fileInfo['content'] = $content;
+ unset( $fileInfo['filePath'] );
+ } elseif ( isset( $fileInfo['callback'] ) ) {
+ $fileInfo['content'] = ( $fileInfo['callback'] )( $context );
+ unset( $fileInfo['callback'] );
+ }
+
+ // Not needed for client response, exists for getDefinitionSummary().
+ unset( $fileInfo['definitionSummary'] );
+ }
+
+ return $expandedPackageFiles;
+ }
+