Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
78.91% |
464 / 588 |
|
47.83% |
22 / 46 |
CRAP | |
0.00% |
0 / 1 |
FileModule | |
78.91% |
464 / 588 |
|
47.83% |
22 / 46 |
705.02 | |
0.00% |
0 / 1 |
__construct | |
77.42% |
48 / 62 |
|
0.00% |
0 / 1 |
45.54 | |||
extractBasePaths | |
63.16% |
12 / 19 |
|
0.00% |
0 / 1 |
9.45 | |||
getScript | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
5 | |||
getScriptURLsForDebug | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
supportsURLLoading | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
shouldSkipStructureTest | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
hasGeneratedScripts | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
42 | |||
getStyles | |
47.06% |
8 / 17 |
|
0.00% |
0 / 1 |
6.37 | |||
getStyleURLsForDebug | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
4.01 | |||
getMessages | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGroup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDependencies | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFileContents | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getSkipFunction | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
requiresES6 | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
enableModuleContentVersion | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFileHashes | |
60.87% |
14 / 23 |
|
0.00% |
0 / 1 |
18.25 | |||
getDefinitionSummary | |
93.94% |
31 / 33 |
|
0.00% |
0 / 1 |
6.01 | |||
getVueComponentParser | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getPath | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getLocalPath | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
getRemotePath | |
71.43% |
5 / 7 |
|
0.00% |
0 / 1 |
4.37 | |||
getStyleSheetLang | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getPackageFileType | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
collateStyleFilesByMedia | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
tryForKey | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
6.10 | |||
getScriptFiles | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
getLanguageScripts | |
33.33% |
4 / 12 |
|
0.00% |
0 / 1 |
12.41 | |||
setSkinStylesOverride | |
58.82% |
10 / 17 |
|
0.00% |
0 / 1 |
10.42 | |||
getStyleFiles | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getSkinStyleFiles | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getAllSkinStyleFiles | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
2 | |||
getAllStyleFiles | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
readStyleFiles | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
4.02 | |||
readStyleFile | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
processStyle | |
95.00% |
19 / 20 |
|
0.00% |
0 / 1 |
5 | |||
getFlip | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getType | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
110 | |||
compileLessString | |
97.30% |
36 / 37 |
|
0.00% |
0 / 1 |
6 | |||
getTemplates | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
expandPackageFiles | |
100.00% |
31 / 31 |
|
100.00% |
1 / 1 |
12 | |||
expandFileInfo | |
81.48% |
66 / 81 |
|
0.00% |
0 / 1 |
22.54 | |||
makeFilePath | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
getPackageFiles | |
90.00% |
9 / 10 |
|
0.00% |
0 / 1 |
4.02 | |||
readFileInfo | |
75.56% |
34 / 45 |
|
0.00% |
0 / 1 |
14.10 | |||
stripBom | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | * @author Trevor Parscal |
20 | * @author Roan Kattouw |
21 | */ |
22 | |
23 | namespace MediaWiki\ResourceLoader; |
24 | |
25 | use CSSJanus; |
26 | use Exception; |
27 | use FileContentsHasher; |
28 | use InvalidArgumentException; |
29 | use LogicException; |
30 | use MediaWiki\Languages\LanguageFallback; |
31 | use MediaWiki\MainConfigNames; |
32 | use MediaWiki\MediaWikiServices; |
33 | use MediaWiki\Output\OutputPage; |
34 | use MediaWiki\Registration\ExtensionRegistry; |
35 | use RuntimeException; |
36 | use Wikimedia\Minify\CSSMin; |
37 | use Wikimedia\RequestTimeout\TimeoutException; |
38 | |
39 | /** |
40 | * Module based on local JavaScript/CSS files. |
41 | * |
42 | * The following public methods can query the database: |
43 | * |
44 | * - getDefinitionSummary / … / Module::getFileDependencies. |
45 | * - getVersionHash / getDefinitionSummary / … / Module::getFileDependencies. |
46 | * - getStyles / Module::saveFileDependencies. |
47 | * |
48 | * @ingroup ResourceLoader |
49 | * @see $wgResourceModules |
50 | * @since 1.17 |
51 | */ |
52 | class FileModule extends Module { |
53 | /** @var string Local base path, see __construct() */ |
54 | protected $localBasePath = ''; |
55 | |
56 | /** @var string Remote base path, see __construct() */ |
57 | protected $remoteBasePath = ''; |
58 | |
59 | /** |
60 | * @var array<int,string|FilePath> List of JavaScript file paths to always include |
61 | */ |
62 | protected $scripts = []; |
63 | |
64 | /** |
65 | * @var array<string,array<int,string|FilePath>> Lists of JavaScript files by language code |
66 | */ |
67 | protected $languageScripts = []; |
68 | |
69 | /** |
70 | * @var array<string,array<int,string|FilePath>> Lists of JavaScript files by skin name |
71 | */ |
72 | protected $skinScripts = []; |
73 | |
74 | /** |
75 | * @var array<int,string|FilePath> List of paths to JavaScript files to include in debug mode |
76 | */ |
77 | protected $debugScripts = []; |
78 | |
79 | /** |
80 | * @var array<int,string|FilePath> List of CSS file files to always include |
81 | */ |
82 | protected $styles = []; |
83 | |
84 | /** |
85 | * @var array<string,array<int,string|FilePath>> Lists of CSS files by skin name |
86 | */ |
87 | protected $skinStyles = []; |
88 | |
89 | /** |
90 | * Packaged files definition, to bundle and make available client-side via `require()`. |
91 | * |
92 | * @see FileModule::expandPackageFiles() |
93 | * @var null|array |
94 | * @phan-var null|array<int,string|FilePath|array{main?:bool,name?:string,file?:string|FilePath,type?:string,content?:mixed,config?:array,callback?:callable,callbackParam?:mixed,versionCallback?:callable}> |
95 | */ |
96 | protected $packageFiles = null; |
97 | |
98 | /** |
99 | * @var array Expanded versions of $packageFiles, lazy-computed by expandPackageFiles(); |
100 | * keyed by context hash |
101 | */ |
102 | private $expandedPackageFiles = []; |
103 | |
104 | /** |
105 | * @var array Further expanded versions of $expandedPackageFiles, lazy-computed by |
106 | * getPackageFiles(); keyed by context hash |
107 | */ |
108 | private $fullyExpandedPackageFiles = []; |
109 | |
110 | /** |
111 | * @var string[] List of modules this module depends on |
112 | */ |
113 | protected $dependencies = []; |
114 | |
115 | /** |
116 | * @var null|string File name containing the body of the skip function |
117 | */ |
118 | protected $skipFunction = null; |
119 | |
120 | /** |
121 | * @var string[] List of message keys used by this module |
122 | */ |
123 | protected $messages = []; |
124 | |
125 | /** @var array<int|string,string|FilePath> List of the named templates used by this module */ |
126 | protected $templates = []; |
127 | |
128 | /** @var null|string Name of group to load this module in */ |
129 | protected $group = null; |
130 | |
131 | /** @var bool Link to raw files in debug mode */ |
132 | protected $debugRaw = true; |
133 | |
134 | /** @var bool Whether CSSJanus flipping should be skipped for this module */ |
135 | protected $noflip = false; |
136 | |
137 | /** @var bool Whether to skip the structure test ResourcesTest::testRespond() */ |
138 | protected $skipStructureTest = false; |
139 | |
140 | /** |
141 | * @var bool Whether getStyleURLsForDebug should return raw file paths, |
142 | * or return load.php urls |
143 | */ |
144 | protected $hasGeneratedStyles = false; |
145 | |
146 | /** |
147 | * @var string[] Place where readStyleFile() tracks file dependencies |
148 | */ |
149 | protected $localFileRefs = []; |
150 | |
151 | /** |
152 | * @var string[] Place where readStyleFile() tracks file dependencies for non-existent files. |
153 | * Used in tests to detect missing dependencies. |
154 | */ |
155 | protected $missingLocalFileRefs = []; |
156 | |
157 | /** |
158 | * @var VueComponentParser|null Lazy-created by getVueComponentParser() |
159 | */ |
160 | protected $vueComponentParser = null; |
161 | |
162 | /** |
163 | * Construct a new module from an options array. |
164 | * |
165 | * @param array $options See $wgResourceModules for the available options. |
166 | * @param string|null $localBasePath Base path to prepend to all local paths in $options. |
167 | * Defaults to MW_INSTALL_PATH |
168 | * @param string|null $remoteBasePath Base path to prepend to all remote paths in $options. |
169 | * Defaults to $wgResourceBasePath |
170 | */ |
171 | public function __construct( |
172 | array $options = [], |
173 | ?string $localBasePath = null, |
174 | ?string $remoteBasePath = null |
175 | ) { |
176 | // Flag to decide whether to automagically add the mediawiki.template module |
177 | $hasTemplates = false; |
178 | // localBasePath and remoteBasePath both have unbelievably long fallback chains |
179 | // and need to be handled separately. |
180 | [ $this->localBasePath, $this->remoteBasePath ] = |
181 | self::extractBasePaths( $options, $localBasePath, $remoteBasePath ); |
182 | |
183 | // Extract, validate and normalise remaining options |
184 | foreach ( $options as $member => $option ) { |
185 | switch ( $member ) { |
186 | // Lists of file paths |
187 | case 'scripts': |
188 | case 'debugScripts': |
189 | case 'styles': |
190 | case 'packageFiles': |
191 | $this->{$member} = is_array( $option ) ? $option : [ $option ]; |
192 | break; |
193 | case 'templates': |
194 | $hasTemplates = true; |
195 | $this->{$member} = is_array( $option ) ? $option : [ $option ]; |
196 | break; |
197 | // Collated lists of file paths |
198 | case 'languageScripts': |
199 | case 'skinScripts': |
200 | case 'skinStyles': |
201 | if ( !is_array( $option ) ) { |
202 | throw new InvalidArgumentException( |
203 | "Invalid collated file path list error. " . |
204 | "'$option' given, array expected." |
205 | ); |
206 | } |
207 | foreach ( $option as $key => $value ) { |
208 | if ( !is_string( $key ) ) { |
209 | throw new InvalidArgumentException( |
210 | "Invalid collated file path list key error. " . |
211 | "'$key' given, string expected." |
212 | ); |
213 | } |
214 | $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ]; |
215 | } |
216 | break; |
217 | case 'deprecated': |
218 | $this->deprecated = $option; |
219 | break; |
220 | // Lists of strings |
221 | case 'dependencies': |
222 | case 'messages': |
223 | // Normalise |
224 | $option = array_values( array_unique( (array)$option ) ); |
225 | sort( $option ); |
226 | |
227 | $this->{$member} = $option; |
228 | break; |
229 | // Single strings |
230 | case 'group': |
231 | case 'skipFunction': |
232 | $this->{$member} = (string)$option; |
233 | break; |
234 | // Single booleans |
235 | case 'debugRaw': |
236 | case 'noflip': |
237 | case 'skipStructureTest': |
238 | $this->{$member} = (bool)$option; |
239 | break; |
240 | } |
241 | } |
242 | if ( isset( $options['scripts'] ) && isset( $options['packageFiles'] ) ) { |
243 | throw new InvalidArgumentException( "A module may not set both 'scripts' and 'packageFiles'" ); |
244 | } |
245 | if ( isset( $options['packageFiles'] ) && isset( $options['skinScripts'] ) ) { |
246 | throw new InvalidArgumentException( "Options 'skinScripts' and 'packageFiles' cannot be used together." ); |
247 | } |
248 | if ( $hasTemplates ) { |
249 | $this->dependencies[] = 'mediawiki.template'; |
250 | // Ensure relevant template compiler module gets loaded |
251 | foreach ( $this->templates as $alias => $templatePath ) { |
252 | if ( is_int( $alias ) ) { |
253 | $alias = $this->getPath( $templatePath ); |
254 | } |
255 | $suffix = explode( '.', $alias ); |
256 | $suffix = end( $suffix ); |
257 | $compilerModule = 'mediawiki.template.' . $suffix; |
258 | if ( $suffix !== 'html' && !in_array( $compilerModule, $this->dependencies ) ) { |
259 | $this->dependencies[] = $compilerModule; |
260 | } |
261 | } |
262 | } |
263 | } |
264 | |
265 | /** |
266 | * Extract a pair of local and remote base paths from module definition information. |
267 | * Implementation note: the amount of global state used in this function is staggering. |
268 | * |
269 | * @param array $options Module definition |
270 | * @param string|null $localBasePath Path to use if not provided in module definition. Defaults |
271 | * to MW_INSTALL_PATH |
272 | * @param string|null $remoteBasePath Path to use if not provided in module definition. Defaults |
273 | * to $wgResourceBasePath |
274 | * @return string[] [ localBasePath, remoteBasePath ] |
275 | */ |
276 | public static function extractBasePaths( |
277 | array $options = [], |
278 | $localBasePath = null, |
279 | $remoteBasePath = null |
280 | ) { |
281 | // The different ways these checks are done, and their ordering, look very silly, |
282 | // but were preserved for backwards-compatibility just in case. Tread lightly. |
283 | |
284 | $remoteBasePath ??= MediaWikiServices::getInstance()->getMainConfig() |
285 | ->get( MainConfigNames::ResourceBasePath ); |
286 | |
287 | if ( isset( $options['remoteExtPath'] ) ) { |
288 | $extensionAssetsPath = MediaWikiServices::getInstance()->getMainConfig() |
289 | ->get( MainConfigNames::ExtensionAssetsPath ); |
290 | $remoteBasePath = $extensionAssetsPath . '/' . $options['remoteExtPath']; |
291 | } |
292 | |
293 | if ( isset( $options['remoteSkinPath'] ) ) { |
294 | $stylePath = MediaWikiServices::getInstance()->getMainConfig() |
295 | ->get( MainConfigNames::StylePath ); |
296 | $remoteBasePath = $stylePath . '/' . $options['remoteSkinPath']; |
297 | } |
298 | |
299 | if ( array_key_exists( 'localBasePath', $options ) ) { |
300 | $localBasePath = (string)$options['localBasePath']; |
301 | } |
302 | |
303 | if ( array_key_exists( 'remoteBasePath', $options ) ) { |
304 | $remoteBasePath = (string)$options['remoteBasePath']; |
305 | } |
306 | |
307 | if ( $localBasePath === null ) { |
308 | $localBasePath = MW_INSTALL_PATH; |
309 | } |
310 | |
311 | if ( $remoteBasePath === '' ) { |
312 | // If MediaWiki is installed at the document root (not recommended), |
313 | // then wgScriptPath is set to the empty string by the installer to |
314 | // ensure safe concatenating of file paths (avoid "/" + "/foo" being "//foo"). |
315 | // However, this also means the path itself can be an invalid URI path, |
316 | // as those must start with a slash. Within ResourceLoader, we will not |
317 | // do such primitive/unsafe slash concatenation and use URI resolution |
318 | // instead, so beyond this point, to avoid fatal errors in CSSMin::resolveUrl(), |
319 | // do a best-effort support for docroot installs by casting this to a slash. |
320 | $remoteBasePath = '/'; |
321 | } |
322 | |
323 | return [ $localBasePath, $remoteBasePath ]; |
324 | } |
325 | |
326 | public function getScript( Context $context ) { |
327 | $packageFiles = $this->getPackageFiles( $context ); |
328 | if ( $packageFiles !== null ) { |
329 | foreach ( $packageFiles['files'] as &$file ) { |
330 | if ( $file['type'] === 'script+style' ) { |
331 | $file['content'] = $file['content']['script']; |
332 | $file['type'] = 'script'; |
333 | } |
334 | } |
335 | return $packageFiles; |
336 | } |
337 | |
338 | $files = $this->getScriptFiles( $context ); |
339 | foreach ( $files as &$file ) { |
340 | $this->readFileInfo( $context, $file ); |
341 | } |
342 | return [ 'plainScripts' => $files ]; |
343 | } |
344 | |
345 | /** |
346 | * @param Context $context |
347 | * @return string[] URLs |
348 | */ |
349 | public function getScriptURLsForDebug( Context $context ) { |
350 | $rl = $context->getResourceLoader(); |
351 | $config = $this->getConfig(); |
352 | $server = $config->get( MainConfigNames::Server ); |
353 | |
354 | $urls = []; |
355 | foreach ( $this->getScriptFiles( $context ) as $file ) { |
356 | if ( isset( $file['filePath'] ) ) { |
357 | $url = OutputPage::transformResourcePath( $config, $this->getRemotePath( $file['filePath'] ) ); |
358 | // Expand debug URL in case we are another wiki's module source (T255367) |
359 | $url = $rl->expandUrl( $server, $url ); |
360 | $urls[] = $url; |
361 | } |
362 | } |
363 | return $urls; |
364 | } |
365 | |
366 | /** |
367 | * @return bool |
368 | */ |
369 | public function supportsURLLoading() { |
370 | // phpcs:ignore Generic.WhiteSpace.LanguageConstructSpacing.IncorrectSingle |
371 | return |
372 | // Denied by options? |
373 | $this->debugRaw |
374 | // If package files are involved, don't support URL loading, because that breaks |
375 | // scoped require() functions |
376 | && !$this->packageFiles |
377 | // Can't link to scripts generated by callbacks |
378 | && !$this->hasGeneratedScripts(); |
379 | } |
380 | |
381 | public function shouldSkipStructureTest() { |
382 | return $this->skipStructureTest || parent::shouldSkipStructureTest(); |
383 | } |
384 | |
385 | /** |
386 | * Determine whether the module may potentially have generated scripts. |
387 | * |
388 | * @return bool |
389 | */ |
390 | private function hasGeneratedScripts() { |
391 | foreach ( |
392 | [ $this->scripts, $this->languageScripts, $this->skinScripts, $this->debugScripts ] |
393 | as $scripts |
394 | ) { |
395 | foreach ( $scripts as $script ) { |
396 | if ( is_array( $script ) ) { |
397 | if ( isset( $script['callback'] ) || isset( $script['versionCallback'] ) ) { |
398 | return true; |
399 | } |
400 | } |
401 | } |
402 | } |
403 | return false; |
404 | } |
405 | |
406 | /** |
407 | * Get all styles for a given context. |
408 | * |
409 | * @param Context $context |
410 | * @return string[] CSS code for $context as an associative array mapping media type to CSS text. |
411 | */ |
412 | public function getStyles( Context $context ) { |
413 | $styles = $this->readStyleFiles( |
414 | $this->getStyleFiles( $context ), |
415 | $context |
416 | ); |
417 | |
418 | $packageFiles = $this->getPackageFiles( $context ); |
419 | if ( $packageFiles !== null ) { |
420 | foreach ( $packageFiles['files'] as $fileName => $file ) { |
421 | if ( $file['type'] === 'script+style' ) { |
422 | $style = $this->processStyle( |
423 | $file['content']['style'], |
424 | $file['content']['styleLang'], |
425 | $fileName, |
426 | $context |
427 | ); |
428 | $styles['all'] = ( $styles['all'] ?? '' ) . "\n" . $style; |
429 | } |
430 | } |
431 | } |
432 | |
433 | // Track indirect file dependencies so that StartUpModule can check for |
434 | // on-disk file changes to any of this files without having to recompute the file list |
435 | $this->saveFileDependencies( $context, $this->localFileRefs ); |
436 | |
437 | return $styles; |
438 | } |
439 | |
440 | /** |
441 | * @param Context $context |
442 | * @return string[][] Lists of URLs by media type |
443 | */ |
444 | public function getStyleURLsForDebug( Context $context ) { |
445 | if ( $this->hasGeneratedStyles ) { |
446 | // Do the default behaviour of returning a url back to load.php |
447 | // but with only=styles. |
448 | return parent::getStyleURLsForDebug( $context ); |
449 | } |
450 | // Our module consists entirely of real css files, |
451 | // in debug mode we can load those directly. |
452 | $urls = []; |
453 | foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) { |
454 | $urls[$mediaType] = []; |
455 | foreach ( $list as $file ) { |
456 | $urls[$mediaType][] = OutputPage::transformResourcePath( |
457 | $this->getConfig(), |
458 | $this->getRemotePath( $file ) |
459 | ); |
460 | } |
461 | } |
462 | return $urls; |
463 | } |
464 | |
465 | /** |
466 | * Get message keys used by this module. |
467 | * |
468 | * @return string[] List of message keys |
469 | */ |
470 | public function getMessages() { |
471 | return $this->messages; |
472 | } |
473 | |
474 | /** |
475 | * Get the name of the group this module should be loaded in. |
476 | * |
477 | * @return null|string Group name |
478 | */ |
479 | public function getGroup() { |
480 | return $this->group; |
481 | } |
482 | |
483 | /** |
484 | * Get names of modules this module depends on. |
485 | * |
486 | * @param Context|null $context |
487 | * @return string[] List of module names |
488 | */ |
489 | public function getDependencies( ?Context $context = null ) { |
490 | return $this->dependencies; |
491 | } |
492 | |
493 | /** |
494 | * Helper method for getting a file. |
495 | * |
496 | * @param string $localPath The path to the resource to load |
497 | * @param string $type The type of resource being loaded (for error reporting only) |
498 | * @return string |
499 | */ |
500 | private function getFileContents( $localPath, $type ) { |
501 | if ( !is_file( $localPath ) ) { |
502 | throw new RuntimeException( "$type file not found or not a file: \"$localPath\"" ); |
503 | } |
504 | return $this->stripBom( file_get_contents( $localPath ) ); |
505 | } |
506 | |
507 | /** |
508 | * @return null|string |
509 | */ |
510 | public function getSkipFunction() { |
511 | if ( !$this->skipFunction ) { |
512 | return null; |
513 | } |
514 | $localPath = $this->getLocalPath( $this->skipFunction ); |
515 | return $this->getFileContents( $localPath, 'skip function' ); |
516 | } |
517 | |
518 | public function requiresES6() { |
519 | return true; |
520 | } |
521 | |
522 | /** |
523 | * Disable module content versioning. |
524 | * |
525 | * This class uses getDefinitionSummary() instead, to avoid filesystem overhead |
526 | * involved with building the full module content inside a startup request. |
527 | * |
528 | * @return bool |
529 | */ |
530 | public function enableModuleContentVersion() { |
531 | return false; |
532 | } |
533 | |
534 | /** |
535 | * Helper method for getDefinitionSummary. |
536 | * |
537 | * @param Context $context |
538 | * @return string Hash |
539 | */ |
540 | private function getFileHashes( Context $context ) { |
541 | $files = []; |
542 | |
543 | foreach ( $this->getStyleFiles( $context ) as $filePaths ) { |
544 | foreach ( $filePaths as $filePath ) { |
545 | $files[] = $this->getLocalPath( $filePath ); |
546 | } |
547 | } |
548 | |
549 | // Extract file paths for package files |
550 | // Optimisation: Use foreach() and isset() instead of array_map/array_filter. |
551 | // This is a hot code path, called by StartupModule for thousands of modules. |
552 | $expandedPackageFiles = $this->expandPackageFiles( $context ); |
553 | if ( $expandedPackageFiles ) { |
554 | foreach ( $expandedPackageFiles['files'] as $fileInfo ) { |
555 | $filePath = $fileInfo['filePath'] ?? $fileInfo['versionFilePath'] ?? null; |
556 | if ( $filePath instanceof FilePath ) { |
557 | $files[] = $filePath->getLocalPath(); |
558 | } |
559 | } |
560 | } |
561 | |
562 | // Add other configured paths |
563 | $scriptFileInfos = $this->getScriptFiles( $context ); |
564 | foreach ( $scriptFileInfos as $fileInfo ) { |
565 | $filePath = $fileInfo['filePath'] ?? $fileInfo['versionFilePath'] ?? null; |
566 | if ( $filePath instanceof FilePath ) { |
567 | $files[] = $filePath->getLocalPath(); |
568 | } |
569 | } |
570 | |
571 | foreach ( $this->templates as $filePath ) { |
572 | $files[] = $this->getLocalPath( $filePath ); |
573 | } |
574 | |
575 | if ( $this->skipFunction ) { |
576 | $files[] = $this->getLocalPath( $this->skipFunction ); |
577 | } |
578 | |
579 | // Add any lazily discovered file dependencies from previous module builds. |
580 | // These are already absolute paths. |
581 | foreach ( $this->getFileDependencies( $context ) as $file ) { |
582 | $files[] = $file; |
583 | } |
584 | |
585 | // Filter out any duplicates. Typically introduced by getFileDependencies() which |
586 | // may lazily re-discover a primary file. |
587 | $files = array_unique( $files ); |
588 | |
589 | // Don't return array keys or any other form of file path here, only the hashes. |
590 | // Including file paths would needlessly cause global cache invalidation when files |
591 | // move on disk or if e.g. the MediaWiki directory name changes. |
592 | // Anything where order is significant is already detected by the definition summary. |
593 | return FileContentsHasher::getFileContentsHash( $files ); |
594 | } |
595 | |
596 | /** |
597 | * Get the definition summary for this module. |
598 | * |
599 | * @param Context $context |
600 | * @return array |
601 | */ |
602 | public function getDefinitionSummary( Context $context ) { |
603 | $summary = parent::getDefinitionSummary( $context ); |
604 | |
605 | $options = []; |
606 | foreach ( [ |
607 | // The following properties are omitted because they don't affect the module response: |
608 | // - localBasePath (Per T104950; Changes when absolute directory name changes. If |
609 | // this affects 'scripts' and other file paths, getFileHashes accounts for that.) |
610 | // - remoteBasePath (Per T104950) |
611 | // - dependencies (provided via startup module) |
612 | // - group (provided via startup module) |
613 | 'styles', |
614 | 'skinStyles', |
615 | 'messages', |
616 | 'templates', |
617 | 'skipFunction', |
618 | 'debugRaw', |
619 | ] as $member ) { |
620 | $options[$member] = $this->{$member}; |
621 | } |
622 | |
623 | $packageFiles = $this->expandPackageFiles( $context ); |
624 | $packageSummaries = []; |
625 | if ( $packageFiles ) { |
626 | // Extract the minimum needed: |
627 | // - The 'main' pointer (included as-is). |
628 | // - The 'files' array, simplified to only which files exist (the keys of |
629 | // this array), and something that represents their non-file content. |
630 | // For packaged files that reflect files directly from disk, the |
631 | // 'getFileHashes' method tracks their content already. |
632 | // It is important that the keys of the $packageFiles['files'] array |
633 | // are preserved, as they do affect the module output. |
634 | foreach ( $packageFiles['files'] as $fileName => $fileInfo ) { |
635 | $packageSummaries[$fileName] = |
636 | $fileInfo['definitionSummary'] ?? $fileInfo['content'] ?? null; |
637 | } |
638 | } |
639 | |
640 | $scriptFiles = $this->getScriptFiles( $context ); |
641 | $scriptSummaries = []; |
642 | foreach ( $scriptFiles as $fileName => $fileInfo ) { |
643 | $scriptSummaries[$fileName] = |
644 | $fileInfo['definitionSummary'] ?? $fileInfo['content'] ?? null; |
645 | } |
646 | |
647 | $summary[] = [ |
648 | 'options' => $options, |
649 | 'packageFiles' => $packageSummaries, |
650 | 'scripts' => $scriptSummaries, |
651 | 'fileHashes' => $this->getFileHashes( $context ), |
652 | 'messageBlob' => $this->getMessageBlob( $context ), |
653 | ]; |
654 | |
655 | $lessVars = $this->getLessVars( $context ); |
656 | if ( $lessVars ) { |
657 | $summary[] = [ 'lessVars' => $lessVars ]; |
658 | } |
659 | |
660 | return $summary; |
661 | } |
662 | |
663 | /** |
664 | * @return VueComponentParser |
665 | */ |
666 | protected function getVueComponentParser() { |
667 | if ( $this->vueComponentParser === null ) { |
668 | $this->vueComponentParser = new VueComponentParser; |
669 | } |
670 | return $this->vueComponentParser; |
671 | } |
672 | |
673 | /** |
674 | * @param string|FilePath $path |
675 | * @return string |
676 | */ |
677 | protected function getPath( $path ) { |
678 | if ( $path instanceof FilePath ) { |
679 | return $path->getPath(); |
680 | } |
681 | |
682 | return $path; |
683 | } |
684 | |
685 | /** |
686 | * @param string|FilePath $path |
687 | * @return string |
688 | */ |
689 | protected function getLocalPath( $path ) { |
690 | if ( $path instanceof FilePath ) { |
691 | if ( $path->getLocalBasePath() !== null ) { |
692 | return $path->getLocalPath(); |
693 | } |
694 | $path = $path->getPath(); |
695 | } |
696 | |
697 | return "{$this->localBasePath}/$path"; |
698 | } |
699 | |
700 | /** |
701 | * @param string|FilePath $path |
702 | * @return string |
703 | */ |
704 | protected function getRemotePath( $path ) { |
705 | if ( $path instanceof FilePath ) { |
706 | if ( $path->getRemoteBasePath() !== null ) { |
707 | return $path->getRemotePath(); |
708 | } |
709 | $path = $path->getPath(); |
710 | } |
711 | |
712 | if ( $this->remoteBasePath === '/' ) { |
713 | return "/$path"; |
714 | } else { |
715 | return "{$this->remoteBasePath}/$path"; |
716 | } |
717 | } |
718 | |
719 | /** |
720 | * Infer the stylesheet language from a stylesheet file path. |
721 | * |
722 | * @since 1.22 |
723 | * @param string $path |
724 | * @return string The stylesheet language name |
725 | */ |
726 | public function getStyleSheetLang( $path ) { |
727 | return preg_match( '/\.less$/i', $path ) ? 'less' : 'css'; |
728 | } |
729 | |
730 | /** |
731 | * Infer the file type from a package file path. |
732 | * |
733 | * @param string $path |
734 | * @return string 'script', 'script-vue', or 'data' |
735 | */ |
736 | public static function getPackageFileType( $path ) { |
737 | if ( preg_match( '/\.json$/i', $path ) ) { |
738 | return 'data'; |
739 | } |
740 | if ( preg_match( '/\.vue$/i', $path ) ) { |
741 | return 'script-vue'; |
742 | } |
743 | return 'script'; |
744 | } |
745 | |
746 | /** |
747 | * Collate style file paths by 'media' option (or 'all' if 'media' is not set) |
748 | * |
749 | * @param array $list List of file paths in any combination of index/path |
750 | * or path/options pairs |
751 | * @return string[][] List of collated file paths |
752 | */ |
753 | private static function collateStyleFilesByMedia( array $list ) { |
754 | $collatedFiles = []; |
755 | foreach ( $list as $key => $value ) { |
756 | if ( is_int( $key ) ) { |
757 | // File name as the value |
758 | $collatedFiles['all'][] = $value; |
759 | } elseif ( is_array( $value ) ) { |
760 | // File name as the key, options array as the value |
761 | $optionValue = $value['media'] ?? 'all'; |
762 | $collatedFiles[$optionValue][] = $key; |
763 | } |
764 | } |
765 | return $collatedFiles; |
766 | } |
767 | |
768 | /** |
769 | * Get a list of element that match a key, optionally using a fallback key. |
770 | * |
771 | * @param array[] $list List of lists to select from |
772 | * @param string $key Key to look for in $list |
773 | * @param string|null $fallback Key to look for in $list if $key doesn't exist |
774 | * @return array List of elements from $list which matched $key or $fallback, |
775 | * or an empty list in case of no match |
776 | */ |
777 | protected static function tryForKey( array $list, $key, $fallback = null ) { |
778 | if ( isset( $list[$key] ) && is_array( $list[$key] ) ) { |
779 | return $list[$key]; |
780 | } elseif ( is_string( $fallback ) |
781 | && isset( $list[$fallback] ) |
782 | && is_array( $list[$fallback] ) |
783 | ) { |
784 | return $list[$fallback]; |
785 | } |
786 | return []; |
787 | } |
788 | |
789 | /** |
790 | * Get script file paths for this module, in order of proper execution. |
791 | * |
792 | * @param Context $context |
793 | * @return array An array of file info arrays as returned by expandFileInfo() |
794 | */ |
795 | private function getScriptFiles( Context $context ): array { |
796 | // List in execution order: scripts, languageScripts, skinScripts, debugScripts. |
797 | // Documented at MediaWiki\MainConfigSchema::ResourceModules. |
798 | $filesByCategory = [ |
799 | 'scripts' => $this->scripts, |
800 | 'languageScripts' => $this->getLanguageScripts( $context->getLanguage() ), |
801 | 'skinScripts' => self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ), |
802 | ]; |
803 | if ( $context->getDebug() ) { |
804 | $filesByCategory['debugScripts'] = $this->debugScripts; |
805 | } |
806 | |
807 | $expandedFiles = []; |
808 | foreach ( $filesByCategory as $category => $files ) { |
809 | foreach ( $files as $key => $fileInfo ) { |
810 | $expandedFileInfo = $this->expandFileInfo( $context, $fileInfo, "$category\[$key]" ); |
811 | $expandedFiles[$expandedFileInfo['name']] = $expandedFileInfo; |
812 | } |
813 | } |
814 | |
815 | return $expandedFiles; |
816 | } |
817 | |
818 | /** |
819 | * Get the set of language scripts for the given language, |
820 | * possibly using a fallback language. |
821 | * |
822 | * @param string $lang |
823 | * @return array<int,string|FilePath> File paths |
824 | */ |
825 | private function getLanguageScripts( string $lang ): array { |
826 | $scripts = self::tryForKey( $this->languageScripts, $lang ); |
827 | if ( $scripts ) { |
828 | return $scripts; |
829 | } |
830 | |
831 | // Optimization: Avoid initialising and calling into language services |
832 | // for the majority of modules that don't use this option. |
833 | if ( $this->languageScripts ) { |
834 | $fallbacks = MediaWikiServices::getInstance() |
835 | ->getLanguageFallback() |
836 | ->getAll( $lang, LanguageFallback::MESSAGES ); |
837 | foreach ( $fallbacks as $lang ) { |
838 | $scripts = self::tryForKey( $this->languageScripts, $lang ); |
839 | if ( $scripts ) { |
840 | return $scripts; |
841 | } |
842 | } |
843 | } |
844 | |
845 | return []; |
846 | } |
847 | |
848 | public function setSkinStylesOverride( array $moduleSkinStyles ): void { |
849 | $moduleName = $this->getName(); |
850 | foreach ( $moduleSkinStyles as $skinName => $overrides ) { |
851 | // If a module provides overrides for a skin, and that skin also provides overrides |
852 | // for the same module, then the module has precedence. |
853 | if ( isset( $this->skinStyles[$skinName] ) ) { |
854 | continue; |
855 | } |
856 | |
857 | // If $moduleName in ResourceModuleSkinStyles is preceded with a '+', the defined style |
858 | // files will be added to 'default' skinStyles, otherwise 'default' will be ignored. |
859 | if ( isset( $overrides[$moduleName] ) ) { |
860 | $paths = (array)$overrides[$moduleName]; |
861 | $styleFiles = []; |
862 | } elseif ( isset( $overrides['+' . $moduleName] ) ) { |
863 | $paths = (array)$overrides['+' . $moduleName]; |
864 | $styleFiles = isset( $this->skinStyles['default'] ) ? |
865 | (array)$this->skinStyles['default'] : |
866 | []; |
867 | } else { |
868 | continue; |
869 | } |
870 | |
871 | // Add new file paths, remapping them to refer to our directories and not use settings |
872 | // from the module we're modifying, which come from the base definition. |
873 | [ $localBasePath, $remoteBasePath ] = self::extractBasePaths( $overrides ); |
874 | |
875 | foreach ( $paths as $path ) { |
876 | $styleFiles[] = new FilePath( $path, $localBasePath, $remoteBasePath ); |
877 | } |
878 | |
879 | $this->skinStyles[$skinName] = $styleFiles; |
880 | } |
881 | } |
882 | |
883 | /** |
884 | * Get a list of file paths for all styles in this module, in order of proper inclusion. |
885 | * |
886 | * @internal Exposed only for use by structure phpunit tests. |
887 | * @param Context $context |
888 | * @return array<string,array<int,string|FilePath>> Map from media type to list of file paths |
889 | */ |
890 | public function getStyleFiles( Context $context ) { |
891 | return array_merge_recursive( |
892 | self::collateStyleFilesByMedia( $this->styles ), |
893 | self::collateStyleFilesByMedia( |
894 | self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ) |
895 | ) |
896 | ); |
897 | } |
898 | |
899 | /** |
900 | * Get a list of file paths for all skin styles in the module used by |
901 | * the skin. |
902 | * |
903 | * @param string $skinName The name of the skin |
904 | * @return array A list of file paths collated by media type |
905 | */ |
906 | protected function getSkinStyleFiles( $skinName ) { |
907 | return self::collateStyleFilesByMedia( |
908 | self::tryForKey( $this->skinStyles, $skinName ) |
909 | ); |
910 | } |
911 | |
912 | /** |
913 | * Get a list of file paths for all skin style files in the module, |
914 | * for all available skins. |
915 | * |
916 | * @return array A list of file paths collated by media type |
917 | */ |
918 | protected function getAllSkinStyleFiles() { |
919 | $skinFactory = MediaWikiServices::getInstance()->getSkinFactory(); |
920 | $styleFiles = []; |
921 | |
922 | $internalSkinNames = array_keys( $skinFactory->getInstalledSkins() ); |
923 | $internalSkinNames[] = 'default'; |
924 | |
925 | foreach ( $internalSkinNames as $internalSkinName ) { |
926 | $styleFiles = array_merge_recursive( |
927 | $styleFiles, |
928 | $this->getSkinStyleFiles( $internalSkinName ) |
929 | ); |
930 | } |
931 | |
932 | return $styleFiles; |
933 | } |
934 | |
935 | /** |
936 | * Get all style files and all skin style files used by this module. |
937 | * |
938 | * @return array |
939 | */ |
940 | public function getAllStyleFiles() { |
941 | $collatedStyleFiles = array_merge_recursive( |
942 | self::collateStyleFilesByMedia( $this->styles ), |
943 | $this->getAllSkinStyleFiles() |
944 | ); |
945 | |
946 | $result = []; |
947 | |
948 | foreach ( $collatedStyleFiles as $styleFiles ) { |
949 | foreach ( $styleFiles as $styleFile ) { |
950 | $result[] = $this->getLocalPath( $styleFile ); |
951 | } |
952 | } |
953 | |
954 | return $result; |
955 | } |
956 | |
957 | /** |
958 | * Read the contents of a list of CSS files and remap and concatenate these. |
959 | * |
960 | * @internal This is considered a private method. Exposed for internal use by WebInstallerOutput. |
961 | * @param array<string,array<int,string|FilePath>> $styles Map of media type to file paths |
962 | * @param Context $context |
963 | * @return array<string,string> Map of combined CSS code, keyed by media type |
964 | */ |
965 | public function readStyleFiles( array $styles, Context $context ) { |
966 | if ( !$styles ) { |
967 | return []; |
968 | } |
969 | foreach ( $styles as $media => $files ) { |
970 | $uniqueFiles = array_unique( $files, SORT_REGULAR ); |
971 | $styleFiles = []; |
972 | foreach ( $uniqueFiles as $file ) { |
973 | $styleFiles[] = $this->readStyleFile( $file, $context ); |
974 | } |
975 | $styles[$media] = implode( "\n", $styleFiles ); |
976 | } |
977 | return $styles; |
978 | } |
979 | |
980 | /** |
981 | * Read and process a style file. Reads a file from disk and runs it through processStyle(). |
982 | * |
983 | * This method can be used as a callback for array_map() |
984 | * |
985 | * @internal |
986 | * @param string|FilePath $path Path of style file to read |
987 | * @param Context $context |
988 | * @return string CSS code |
989 | */ |
990 | protected function readStyleFile( $path, Context $context ) { |
991 | $localPath = $this->getLocalPath( $path ); |
992 | $style = $this->getFileContents( $localPath, 'style' ); |
993 | $styleLang = $this->getStyleSheetLang( $localPath ); |
994 | |
995 | return $this->processStyle( $style, $styleLang, $path, $context ); |
996 | } |
997 | |
998 | /** |
999 | * Process a CSS/LESS string. |
1000 | * |
1001 | * This method performs the following processing steps: |
1002 | * - LESS compilation (if $styleLang = 'less') |
1003 | * - RTL flipping with CSSJanus (if getFlip() returns true) |
1004 | * - Registration of references to local files in $localFileRefs and $missingLocalFileRefs |
1005 | * - URL remapping and data URI embedding |
1006 | * |
1007 | * @internal |
1008 | * @param string $style CSS or LESS code |
1009 | * @param string $styleLang Language of $style code ('css' or 'less') |
1010 | * @param string|FilePath $path Path to code file, used for resolving relative file paths |
1011 | * @param Context $context |
1012 | * @return string Processed CSS code |
1013 | */ |
1014 | protected function processStyle( $style, $styleLang, $path, Context $context ) { |
1015 | $localPath = $this->getLocalPath( $path ); |
1016 | $remotePath = $this->getRemotePath( $path ); |
1017 | |
1018 | if ( $styleLang === 'less' ) { |
1019 | $style = $this->compileLessString( $style, $localPath, $context ); |
1020 | $this->hasGeneratedStyles = true; |
1021 | } |
1022 | |
1023 | if ( $this->getFlip( $context ) ) { |
1024 | $style = CSSJanus::transform( |
1025 | $style, |
1026 | /* $swapLtrRtlInURL = */ true, |
1027 | /* $swapLeftRightInURL = */ false |
1028 | ); |
1029 | $this->hasGeneratedStyles = true; |
1030 | } |
1031 | |
1032 | $localDir = dirname( $localPath ); |
1033 | $remoteDir = dirname( $remotePath ); |
1034 | // Get and register local file references |
1035 | $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir ); |
1036 | foreach ( $localFileRefs as $file ) { |
1037 | if ( is_file( $file ) ) { |
1038 | $this->localFileRefs[] = $file; |
1039 | } else { |
1040 | $this->missingLocalFileRefs[] = $file; |
1041 | } |
1042 | } |
1043 | // Don't cache this call. remap() ensures data URIs embeds are up to date, |
1044 | // and urls contain correct content hashes in their query string. (T128668) |
1045 | return CSSMin::remap( $style, $localDir, $remoteDir, true ); |
1046 | } |
1047 | |
1048 | /** |
1049 | * Get whether CSS for this module should be flipped |
1050 | * @param Context $context |
1051 | * @return bool |
1052 | */ |
1053 | public function getFlip( Context $context ) { |
1054 | return $context->getDirection() === 'rtl' && !$this->noflip; |
1055 | } |
1056 | |
1057 | /** |
1058 | * Get the module's load type. |
1059 | * |
1060 | * @since 1.28 |
1061 | * @return string |
1062 | */ |
1063 | public function getType() { |
1064 | $canBeStylesOnly = !( |
1065 | // All options except 'styles', 'skinStyles' and 'debugRaw' |
1066 | $this->scripts |
1067 | || $this->debugScripts |
1068 | || $this->templates |
1069 | || $this->languageScripts |
1070 | || $this->skinScripts |
1071 | || $this->dependencies |
1072 | || $this->messages |
1073 | || $this->skipFunction |
1074 | || $this->packageFiles |
1075 | ); |
1076 | return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL; |
1077 | } |
1078 | |
1079 | /** |
1080 | * Compile a LESS string into CSS. |
1081 | * |
1082 | * Keeps track of all used files and adds them to localFileRefs. |
1083 | * |
1084 | * @since 1.35 |
1085 | * @param string $style LESS source to compile |
1086 | * @param string $stylePath File path of LESS source, used for resolving relative file paths |
1087 | * @param Context $context Context in which to generate script |
1088 | * @return string CSS source |
1089 | */ |
1090 | protected function compileLessString( $style, $stylePath, Context $context ) { |
1091 | static $cache; |
1092 | // @TODO: dependency injection |
1093 | if ( !$cache ) { |
1094 | $cache = MediaWikiServices::getInstance()->getObjectCacheFactory() |
1095 | ->getLocalServerInstance( CACHE_HASH ); |
1096 | } |
1097 | |
1098 | $skinName = $context->getSkin(); |
1099 | $skinImportPaths = ExtensionRegistry::getInstance()->getAttribute( 'SkinLessImportPaths' ); |
1100 | $importDirs = []; |
1101 | if ( isset( $skinImportPaths[ $skinName ] ) ) { |
1102 | $importDirs[] = $skinImportPaths[ $skinName ]; |
1103 | } |
1104 | |
1105 | $vars = $this->getLessVars( $context ); |
1106 | // Construct a cache key from a hash of the LESS source, and a hash digest |
1107 | // of the LESS variables and import dirs used for compilation. |
1108 | ksort( $vars ); |
1109 | $compilerParams = [ |
1110 | 'vars' => $vars, |
1111 | 'importDirs' => $importDirs, |
1112 | // CodexDevelopmentDir affects import path mapping in ResourceLoader::getLessCompiler(), |
1113 | // so take that into account too |
1114 | 'codexDevDir' => $this->getConfig()->get( MainConfigNames::CodexDevelopmentDir ) |
1115 | ]; |
1116 | $key = $cache->makeGlobalKey( |
1117 | 'resourceloader-less', |
1118 | 'v1', |
1119 | hash( 'md4', $style ), |
1120 | hash( 'md4', serialize( $compilerParams ) ) |
1121 | ); |
1122 | |
1123 | // If we got a cached value, we have to validate it by getting a checksum of all the |
1124 | // files that were loaded by the parser and ensuring it matches the cached entry's. |
1125 | $data = $cache->get( $key ); |
1126 | if ( |
1127 | !$data || |
1128 | $data['hash'] !== FileContentsHasher::getFileContentsHash( $data['files'] ) |
1129 | ) { |
1130 | $compiler = $context->getResourceLoader()->getLessCompiler( $vars, $importDirs ); |
1131 | |
1132 | $css = $compiler->parse( $style, $stylePath )->getCss(); |
1133 | // T253055: store the implicit dependency paths in a form relative to any install |
1134 | // path so that multiple version of the application can share the cache for identical |
1135 | // less stylesheets. This also avoids churn during application updates. |
1136 | $files = $compiler->getParsedFiles(); |
1137 | $data = [ |
1138 | 'css' => $css, |
1139 | 'files' => Module::getRelativePaths( $files ), |
1140 | 'hash' => FileContentsHasher::getFileContentsHash( $files ) |
1141 | ]; |
1142 | $cache->set( $key, $data, $cache::TTL_DAY ); |
1143 | } |
1144 | |
1145 | foreach ( Module::expandRelativePaths( $data['files'] ) as $path ) { |
1146 | $this->localFileRefs[] = $path; |
1147 | } |
1148 | |
1149 | return $data['css']; |
1150 | } |
1151 | |
1152 | /** |
1153 | * Get content of named templates for this module. |
1154 | * |
1155 | * @return array<string,string> Templates mapping template alias to content |
1156 | */ |
1157 | public function getTemplates() { |
1158 | $templates = []; |
1159 | |
1160 | foreach ( $this->templates as $alias => $templatePath ) { |
1161 | // Alias is optional |
1162 | if ( is_int( $alias ) ) { |
1163 | $alias = $this->getPath( $templatePath ); |
1164 | } |
1165 | $localPath = $this->getLocalPath( $templatePath ); |
1166 | $content = $this->getFileContents( $localPath, 'template' ); |
1167 | |
1168 | $templates[$alias] = $this->stripBom( $content ); |
1169 | } |
1170 | return $templates; |
1171 | } |
1172 | |
1173 | /** |
1174 | * Internal helper for use by getPackageFiles(), getFileHashes() and getDefinitionSummary(). |
1175 | * |
1176 | * This expands the 'packageFiles' definition into something that's (almost) the right format |
1177 | * for getPackageFiles() to return. It expands shorthands, resolves config vars, and handles |
1178 | * summarising any non-file data for getVersionHash(). For file-based data, getFileHashes() |
1179 | * handles it instead, which also ends up in getDefinitionSummary(). |
1180 | * |
1181 | * What it does not do is reading the actual contents of any specified files, nor invoking |
1182 | * the computation callbacks. Those things are done by getPackageFiles() instead to improve |
1183 | * backend performance by only doing this work when the module response is needed, and not |
1184 | * when merely computing the version hash for StartupModule, or when checking |
1185 | * If-None-Match headers for a HTTP 304 response. |
1186 | * |
1187 | * @param Context $context |
1188 | * @return array|null Array of arrays as returned by expandFileInfo(), with the key being |
1189 | * the file name, or null if this is not a package file module. |
1190 | * @phan-return array{main:?string,files:array[]}|null |
1191 | */ |
1192 | private function expandPackageFiles( Context $context ) { |