]> scripts.mit.edu Git - autoinstalls/mediawiki.git/blob - includes/resourceloader/ResourceLoaderFileModule.php
MediaWiki 1.30.2-scripts2
[autoinstalls/mediawiki.git] / includes / resourceloader / ResourceLoaderFileModule.php
1 <?php
2 /**
3  * ResourceLoader module based on local JavaScript/CSS files.
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License along
16  * with this program; if not, write to the Free Software Foundation, Inc.,
17  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18  * http://www.gnu.org/copyleft/gpl.html
19  *
20  * @file
21  * @author Trevor Parscal
22  * @author Roan Kattouw
23  */
24
25 /**
26  * ResourceLoader module based on local JavaScript/CSS files.
27  */
28 class ResourceLoaderFileModule extends ResourceLoaderModule {
29         /* Protected Members */
30
31         /** @var string Local base path, see __construct() */
32         protected $localBasePath = '';
33
34         /** @var string Remote base path, see __construct() */
35         protected $remoteBasePath = '';
36
37         /** @var array Saves a list of the templates named by the modules. */
38         protected $templates = [];
39
40         /**
41          * @var array List of paths to JavaScript files to always include
42          * @par Usage:
43          * @code
44          * [ [file-path], [file-path], ... ]
45          * @endcode
46          */
47         protected $scripts = [];
48
49         /**
50          * @var array List of JavaScript files to include when using a specific language
51          * @par Usage:
52          * @code
53          * [ [language-code] => [ [file-path], [file-path], ... ], ... ]
54          * @endcode
55          */
56         protected $languageScripts = [];
57
58         /**
59          * @var array List of JavaScript files to include when using a specific skin
60          * @par Usage:
61          * @code
62          * [ [skin-name] => [ [file-path], [file-path], ... ], ... ]
63          * @endcode
64          */
65         protected $skinScripts = [];
66
67         /**
68          * @var array List of paths to JavaScript files to include in debug mode
69          * @par Usage:
70          * @code
71          * [ [skin-name] => [ [file-path], [file-path], ... ], ... ]
72          * @endcode
73          */
74         protected $debugScripts = [];
75
76         /**
77          * @var array List of paths to CSS files to always include
78          * @par Usage:
79          * @code
80          * [ [file-path], [file-path], ... ]
81          * @endcode
82          */
83         protected $styles = [];
84
85         /**
86          * @var array List of paths to CSS files to include when using specific skins
87          * @par Usage:
88          * @code
89          * [ [file-path], [file-path], ... ]
90          * @endcode
91          */
92         protected $skinStyles = [];
93
94         /**
95          * @var array List of modules this module depends on
96          * @par Usage:
97          * @code
98          * [ [file-path], [file-path], ... ]
99          * @endcode
100          */
101         protected $dependencies = [];
102
103         /**
104          * @var string File name containing the body of the skip function
105          */
106         protected $skipFunction = null;
107
108         /**
109          * @var array List of message keys used by this module
110          * @par Usage:
111          * @code
112          * [ [message-key], [message-key], ... ]
113          * @endcode
114          */
115         protected $messages = [];
116
117         /** @var string Name of group to load this module in */
118         protected $group;
119
120         /** @var bool Link to raw files in debug mode */
121         protected $debugRaw = true;
122
123         /** @var bool Whether mw.loader.state() call should be omitted */
124         protected $raw = false;
125
126         protected $targets = [ 'desktop' ];
127
128         /** @var bool Whether CSSJanus flipping should be skipped for this module */
129         protected $noflip = false;
130
131         /**
132          * @var bool Whether getStyleURLsForDebug should return raw file paths,
133          * or return load.php urls
134          */
135         protected $hasGeneratedStyles = false;
136
137         /**
138          * @var array Place where readStyleFile() tracks file dependencies
139          * @par Usage:
140          * @code
141          * [ [file-path], [file-path], ... ]
142          * @endcode
143          */
144         protected $localFileRefs = [];
145
146         /**
147          * @var array Place where readStyleFile() tracks file dependencies for non-existent files.
148          * Used in tests to detect missing dependencies.
149          */
150         protected $missingLocalFileRefs = [];
151
152         /* Methods */
153
154         /**
155          * Constructs a new module from an options array.
156          *
157          * @param array $options List of options; if not given or empty, an empty module will be
158          *     constructed
159          * @param string $localBasePath Base path to prepend to all local paths in $options. Defaults
160          *     to $IP
161          * @param string $remoteBasePath Base path to prepend to all remote paths in $options. Defaults
162          *     to $wgResourceBasePath
163          *
164          * Below is a description for the $options array:
165          * @throws InvalidArgumentException
166          * @par Construction options:
167          * @code
168          *     [
169          *         // Base path to prepend to all local paths in $options. Defaults to $IP
170          *         'localBasePath' => [base path],
171          *         // Base path to prepend to all remote paths in $options. Defaults to $wgResourceBasePath
172          *         'remoteBasePath' => [base path],
173          *         // Equivalent of remoteBasePath, but relative to $wgExtensionAssetsPath
174          *         'remoteExtPath' => [base path],
175          *         // Equivalent of remoteBasePath, but relative to $wgStylePath
176          *         'remoteSkinPath' => [base path],
177          *         // Scripts to always include
178          *         'scripts' => [file path string or array of file path strings],
179          *         // Scripts to include in specific language contexts
180          *         'languageScripts' => [
181          *             [language code] => [file path string or array of file path strings],
182          *         ],
183          *         // Scripts to include in specific skin contexts
184          *         'skinScripts' => [
185          *             [skin name] => [file path string or array of file path strings],
186          *         ],
187          *         // Scripts to include in debug contexts
188          *         'debugScripts' => [file path string or array of file path strings],
189          *         // Modules which must be loaded before this module
190          *         'dependencies' => [module name string or array of module name strings],
191          *         'templates' => [
192          *             [template alias with file.ext] => [file path to a template file],
193          *         ],
194          *         // Styles to always load
195          *         'styles' => [file path string or array of file path strings],
196          *         // Styles to include in specific skin contexts
197          *         'skinStyles' => [
198          *             [skin name] => [file path string or array of file path strings],
199          *         ],
200          *         // Messages to always load
201          *         'messages' => [array of message key strings],
202          *         // Group which this module should be loaded together with
203          *         'group' => [group name string],
204          *         // Function that, if it returns true, makes the loader skip this module.
205          *         // The file must contain valid JavaScript for execution in a private function.
206          *         // The file must not contain the "function () {" and "}" wrapper though.
207          *         'skipFunction' => [file path]
208          *     ]
209          * @endcode
210          */
211         public function __construct(
212                 $options = [],
213                 $localBasePath = null,
214                 $remoteBasePath = null
215         ) {
216                 // Flag to decide whether to automagically add the mediawiki.template module
217                 $hasTemplates = false;
218                 // localBasePath and remoteBasePath both have unbelievably long fallback chains
219                 // and need to be handled separately.
220                 list( $this->localBasePath, $this->remoteBasePath ) =
221                         self::extractBasePaths( $options, $localBasePath, $remoteBasePath );
222
223                 // Extract, validate and normalise remaining options
224                 foreach ( $options as $member => $option ) {
225                         switch ( $member ) {
226                                 // Lists of file paths
227                                 case 'scripts':
228                                 case 'debugScripts':
229                                 case 'styles':
230                                         $this->{$member} = (array)$option;
231                                         break;
232                                 case 'templates':
233                                         $hasTemplates = true;
234                                         $this->{$member} = (array)$option;
235                                         break;
236                                 // Collated lists of file paths
237                                 case 'languageScripts':
238                                 case 'skinScripts':
239                                 case 'skinStyles':
240                                         if ( !is_array( $option ) ) {
241                                                 throw new InvalidArgumentException(
242                                                         "Invalid collated file path list error. " .
243                                                         "'$option' given, array expected."
244                                                 );
245                                         }
246                                         foreach ( $option as $key => $value ) {
247                                                 if ( !is_string( $key ) ) {
248                                                         throw new InvalidArgumentException(
249                                                                 "Invalid collated file path list key error. " .
250                                                                 "'$key' given, string expected."
251                                                         );
252                                                 }
253                                                 $this->{$member}[$key] = (array)$value;
254                                         }
255                                         break;
256                                 case 'deprecated':
257                                         $this->deprecated = $option;
258                                         break;
259                                 // Lists of strings
260                                 case 'dependencies':
261                                 case 'messages':
262                                 case 'targets':
263                                         // Normalise
264                                         $option = array_values( array_unique( (array)$option ) );
265                                         sort( $option );
266
267                                         $this->{$member} = $option;
268                                         break;
269                                 // Single strings
270                                 case 'group':
271                                 case 'skipFunction':
272                                         $this->{$member} = (string)$option;
273                                         break;
274                                 // Single booleans
275                                 case 'debugRaw':
276                                 case 'raw':
277                                 case 'noflip':
278                                         $this->{$member} = (bool)$option;
279                                         break;
280                         }
281                 }
282                 if ( $hasTemplates ) {
283                         $this->dependencies[] = 'mediawiki.template';
284                         // Ensure relevant template compiler module gets loaded
285                         foreach ( $this->templates as $alias => $templatePath ) {
286                                 if ( is_int( $alias ) ) {
287                                         $alias = $templatePath;
288                                 }
289                                 $suffix = explode( '.', $alias );
290                                 $suffix = end( $suffix );
291                                 $compilerModule = 'mediawiki.template.' . $suffix;
292                                 if ( $suffix !== 'html' && !in_array( $compilerModule, $this->dependencies ) ) {
293                                         $this->dependencies[] = $compilerModule;
294                                 }
295                         }
296                 }
297         }
298
299         /**
300          * Extract a pair of local and remote base paths from module definition information.
301          * Implementation note: the amount of global state used in this function is staggering.
302          *
303          * @param array $options Module definition
304          * @param string $localBasePath Path to use if not provided in module definition. Defaults
305          *     to $IP
306          * @param string $remoteBasePath Path to use if not provided in module definition. Defaults
307          *     to $wgResourceBasePath
308          * @return array Array( localBasePath, remoteBasePath )
309          */
310         public static function extractBasePaths(
311                 $options = [],
312                 $localBasePath = null,
313                 $remoteBasePath = null
314         ) {
315                 global $IP, $wgResourceBasePath;
316
317                 // The different ways these checks are done, and their ordering, look very silly,
318                 // but were preserved for backwards-compatibility just in case. Tread lightly.
319
320                 if ( $localBasePath === null ) {
321                         $localBasePath = $IP;
322                 }
323                 if ( $remoteBasePath === null ) {
324                         $remoteBasePath = $wgResourceBasePath;
325                 }
326
327                 if ( isset( $options['remoteExtPath'] ) ) {
328                         global $wgExtensionAssetsPath;
329                         $remoteBasePath = $wgExtensionAssetsPath . '/' . $options['remoteExtPath'];
330                 }
331
332                 if ( isset( $options['remoteSkinPath'] ) ) {
333                         global $wgStylePath;
334                         $remoteBasePath = $wgStylePath . '/' . $options['remoteSkinPath'];
335                 }
336
337                 if ( array_key_exists( 'localBasePath', $options ) ) {
338                         $localBasePath = (string)$options['localBasePath'];
339                 }
340
341                 if ( array_key_exists( 'remoteBasePath', $options ) ) {
342                         $remoteBasePath = (string)$options['remoteBasePath'];
343                 }
344
345                 return [ $localBasePath, $remoteBasePath ];
346         }
347
348         /**
349          * Gets all scripts for a given context concatenated together.
350          *
351          * @param ResourceLoaderContext $context Context in which to generate script
352          * @return string JavaScript code for $context
353          */
354         public function getScript( ResourceLoaderContext $context ) {
355                 $files = $this->getScriptFiles( $context );
356                 return $this->getDeprecationInformation() . $this->readScriptFiles( $files );
357         }
358
359         /**
360          * @param ResourceLoaderContext $context
361          * @return array
362          */
363         public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
364                 $urls = [];
365                 foreach ( $this->getScriptFiles( $context ) as $file ) {
366                         $urls[] = OutputPage::transformResourcePath(
367                                 $this->getConfig(),
368                                 $this->getRemotePath( $file )
369                         );
370                 }
371                 return $urls;
372         }
373
374         /**
375          * @return bool
376          */
377         public function supportsURLLoading() {
378                 return $this->debugRaw;
379         }
380
381         /**
382          * Get all styles for a given context.
383          *
384          * @param ResourceLoaderContext $context
385          * @return array CSS code for $context as an associative array mapping media type to CSS text.
386          */
387         public function getStyles( ResourceLoaderContext $context ) {
388                 $styles = $this->readStyleFiles(
389                         $this->getStyleFiles( $context ),
390                         $this->getFlip( $context ),
391                         $context
392                 );
393                 // Collect referenced files
394                 $this->saveFileDependencies( $context, $this->localFileRefs );
395
396                 return $styles;
397         }
398
399         /**
400          * @param ResourceLoaderContext $context
401          * @return array
402          */
403         public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
404                 if ( $this->hasGeneratedStyles ) {
405                         // Do the default behaviour of returning a url back to load.php
406                         // but with only=styles.
407                         return parent::getStyleURLsForDebug( $context );
408                 }
409                 // Our module consists entirely of real css files,
410                 // in debug mode we can load those directly.
411                 $urls = [];
412                 foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) {
413                         $urls[$mediaType] = [];
414                         foreach ( $list as $file ) {
415                                 $urls[$mediaType][] = OutputPage::transformResourcePath(
416                                         $this->getConfig(),
417                                         $this->getRemotePath( $file )
418                                 );
419                         }
420                 }
421                 return $urls;
422         }
423
424         /**
425          * Gets list of message keys used by this module.
426          *
427          * @return array List of message keys
428          */
429         public function getMessages() {
430                 return $this->messages;
431         }
432
433         /**
434          * Gets the name of the group this module should be loaded in.
435          *
436          * @return string Group name
437          */
438         public function getGroup() {
439                 return $this->group;
440         }
441
442         /**
443          * Gets list of names of modules this module depends on.
444          * @param ResourceLoaderContext|null $context
445          * @return array List of module names
446          */
447         public function getDependencies( ResourceLoaderContext $context = null ) {
448                 return $this->dependencies;
449         }
450
451         /**
452          * Get the skip function.
453          * @return null|string
454          * @throws MWException
455          */
456         public function getSkipFunction() {
457                 if ( !$this->skipFunction ) {
458                         return null;
459                 }
460
461                 $localPath = $this->getLocalPath( $this->skipFunction );
462                 if ( !file_exists( $localPath ) ) {
463                         throw new MWException( __METHOD__ . ": skip function file not found: \"$localPath\"" );
464                 }
465                 $contents = $this->stripBom( file_get_contents( $localPath ) );
466                 if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
467                         $contents = $this->validateScriptFile( $localPath, $contents );
468                 }
469                 return $contents;
470         }
471
472         /**
473          * @return bool
474          */
475         public function isRaw() {
476                 return $this->raw;
477         }
478
479         /**
480          * Disable module content versioning.
481          *
482          * This class uses getDefinitionSummary() instead, to avoid filesystem overhead
483          * involved with building the full module content inside a startup request.
484          *
485          * @return bool
486          */
487         public function enableModuleContentVersion() {
488                 return false;
489         }
490
491         /**
492          * Helper method to gather file hashes for getDefinitionSummary.
493          *
494          * This function is context-sensitive, only computing hashes of files relevant to the
495          * given language, skin, etc.
496          *
497          * @see ResourceLoaderModule::getFileDependencies
498          * @param ResourceLoaderContext $context
499          * @return array
500          */
501         protected function getFileHashes( ResourceLoaderContext $context ) {
502                 $files = [];
503
504                 // Flatten style files into $files
505                 $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' );
506                 foreach ( $styles as $styleFiles ) {
507                         $files = array_merge( $files, $styleFiles );
508                 }
509
510                 $skinFiles = self::collateFilePathListByOption(
511                         self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
512                         'media',
513                         'all'
514                 );
515                 foreach ( $skinFiles as $styleFiles ) {
516                         $files = array_merge( $files, $styleFiles );
517                 }
518
519                 // Final merge, this should result in a master list of dependent files
520                 $files = array_merge(
521                         $files,
522                         $this->scripts,
523                         $this->templates,
524                         $context->getDebug() ? $this->debugScripts : [],
525                         $this->getLanguageScripts( $context->getLanguage() ),
526                         self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
527                 );
528                 if ( $this->skipFunction ) {
529                         $files[] = $this->skipFunction;
530                 }
531                 $files = array_map( [ $this, 'getLocalPath' ], $files );
532                 // File deps need to be treated separately because they're already prefixed
533                 $files = array_merge( $files, $this->getFileDependencies( $context ) );
534                 // Filter out any duplicates from getFileDependencies() and others.
535                 // Most commonly introduced by compileLessFile(), which always includes the
536                 // entry point Less file we already know about.
537                 $files = array_values( array_unique( $files ) );
538
539                 // Don't include keys or file paths here, only the hashes. Including that would needlessly
540                 // cause global cache invalidation when files move or if e.g. the MediaWiki path changes.
541                 // Any significant ordering is already detected by the definition summary.
542                 return array_map( [ __CLASS__, 'safeFileHash' ], $files );
543         }
544
545         /**
546          * Get the definition summary for this module.
547          *
548          * @param ResourceLoaderContext $context
549          * @return array
550          */
551         public function getDefinitionSummary( ResourceLoaderContext $context ) {
552                 $summary = parent::getDefinitionSummary( $context );
553
554                 $options = [];
555                 foreach ( [
556                         // The following properties are omitted because they don't affect the module reponse:
557                         // - localBasePath (Per T104950; Changes when absolute directory name changes. If
558                         //    this affects 'scripts' and other file paths, getFileHashes accounts for that.)
559                         // - remoteBasePath (Per T104950)
560                         // - dependencies (provided via startup module)
561                         // - targets
562                         // - group (provided via startup module)
563                         'scripts',
564                         'debugScripts',
565                         'styles',
566                         'languageScripts',
567                         'skinScripts',
568                         'skinStyles',
569                         'messages',
570                         'templates',
571                         'skipFunction',
572                         'debugRaw',
573                         'raw',
574                 ] as $member ) {
575                         $options[$member] = $this->{$member};
576                 };
577
578                 $summary[] = [
579                         'options' => $options,
580                         'fileHashes' => $this->getFileHashes( $context ),
581                         'messageBlob' => $this->getMessageBlob( $context ),
582                 ];
583
584                 $lessVars = $this->getLessVars( $context );
585                 if ( $lessVars ) {
586                         $summary[] = [ 'lessVars' => $lessVars ];
587                 }
588
589                 return $summary;
590         }
591
592         /**
593          * @param string|ResourceLoaderFilePath $path
594          * @return string
595          */
596         protected function getLocalPath( $path ) {
597                 if ( $path instanceof ResourceLoaderFilePath ) {
598                         return $path->getLocalPath();
599                 }
600
601                 return "{$this->localBasePath}/$path";
602         }
603
604         /**
605          * @param string|ResourceLoaderFilePath $path
606          * @return string
607          */
608         protected function getRemotePath( $path ) {
609                 if ( $path instanceof ResourceLoaderFilePath ) {
610                         return $path->getRemotePath();
611                 }
612
613                 return "{$this->remoteBasePath}/$path";
614         }
615
616         /**
617          * Infer the stylesheet language from a stylesheet file path.
618          *
619          * @since 1.22
620          * @param string $path
621          * @return string The stylesheet language name
622          */
623         public function getStyleSheetLang( $path ) {
624                 return preg_match( '/\.less$/i', $path ) ? 'less' : 'css';
625         }
626
627         /**
628          * Collates file paths by option (where provided).
629          *
630          * @param array $list List of file paths in any combination of index/path
631          *     or path/options pairs
632          * @param string $option Option name
633          * @param mixed $default Default value if the option isn't set
634          * @return array List of file paths, collated by $option
635          */
636         protected static function collateFilePathListByOption( array $list, $option, $default ) {
637                 $collatedFiles = [];
638                 foreach ( (array)$list as $key => $value ) {
639                         if ( is_int( $key ) ) {
640                                 // File name as the value
641                                 if ( !isset( $collatedFiles[$default] ) ) {
642                                         $collatedFiles[$default] = [];
643                                 }
644                                 $collatedFiles[$default][] = $value;
645                         } elseif ( is_array( $value ) ) {
646                                 // File name as the key, options array as the value
647                                 $optionValue = isset( $value[$option] ) ? $value[$option] : $default;
648                                 if ( !isset( $collatedFiles[$optionValue] ) ) {
649                                         $collatedFiles[$optionValue] = [];
650                                 }
651                                 $collatedFiles[$optionValue][] = $key;
652                         }
653                 }
654                 return $collatedFiles;
655         }
656
657         /**
658          * Get a list of element that match a key, optionally using a fallback key.
659          *
660          * @param array $list List of lists to select from
661          * @param string $key Key to look for in $map
662          * @param string $fallback Key to look for in $list if $key doesn't exist
663          * @return array List of elements from $map which matched $key or $fallback,
664          *  or an empty list in case of no match
665          */
666         protected static function tryForKey( array $list, $key, $fallback = null ) {
667                 if ( isset( $list[$key] ) && is_array( $list[$key] ) ) {
668                         return $list[$key];
669                 } elseif ( is_string( $fallback )
670                         && isset( $list[$fallback] )
671                         && is_array( $list[$fallback] )
672                 ) {
673                         return $list[$fallback];
674                 }
675                 return [];
676         }
677
678         /**
679          * Get a list of file paths for all scripts in this module, in order of proper execution.
680          *
681          * @param ResourceLoaderContext $context
682          * @return array List of file paths
683          */
684         protected function getScriptFiles( ResourceLoaderContext $context ) {
685                 $files = array_merge(
686                         $this->scripts,
687                         $this->getLanguageScripts( $context->getLanguage() ),
688                         self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' )
689                 );
690                 if ( $context->getDebug() ) {
691                         $files = array_merge( $files, $this->debugScripts );
692                 }
693
694                 return array_unique( $files, SORT_REGULAR );
695         }
696
697         /**
698          * Get the set of language scripts for the given language,
699          * possibly using a fallback language.
700          *
701          * @param string $lang
702          * @return array
703          */
704         private function getLanguageScripts( $lang ) {
705                 $scripts = self::tryForKey( $this->languageScripts, $lang );
706                 if ( $scripts ) {
707                         return $scripts;
708                 }
709                 $fallbacks = Language::getFallbacksFor( $lang );
710                 foreach ( $fallbacks as $lang ) {
711                         $scripts = self::tryForKey( $this->languageScripts, $lang );
712                         if ( $scripts ) {
713                                 return $scripts;
714                         }
715                 }
716
717                 return [];
718         }
719
720         /**
721          * Get a list of file paths for all styles in this module, in order of proper inclusion.
722          *
723          * @param ResourceLoaderContext $context
724          * @return array List of file paths
725          */
726         public function getStyleFiles( ResourceLoaderContext $context ) {
727                 return array_merge_recursive(
728                         self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
729                         self::collateFilePathListByOption(
730                                 self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ),
731                                 'media',
732                                 'all'
733                         )
734                 );
735         }
736
737         /**
738          * Gets a list of file paths for all skin styles in the module used by
739          * the skin.
740          *
741          * @param string $skinName The name of the skin
742          * @return array A list of file paths collated by media type
743          */
744         protected function getSkinStyleFiles( $skinName ) {
745                 return self::collateFilePathListByOption(
746                         self::tryForKey( $this->skinStyles, $skinName ),
747                         'media',
748                         'all'
749                 );
750         }
751
752         /**
753          * Gets a list of file paths for all skin style files in the module,
754          * for all available skins.
755          *
756          * @return array A list of file paths collated by media type
757          */
758         protected function getAllSkinStyleFiles() {
759                 $styleFiles = [];
760                 $internalSkinNames = array_keys( Skin::getSkinNames() );
761                 $internalSkinNames[] = 'default';
762
763                 foreach ( $internalSkinNames as $internalSkinName ) {
764                         $styleFiles = array_merge_recursive(
765                                 $styleFiles,
766                                 $this->getSkinStyleFiles( $internalSkinName )
767                         );
768                 }
769
770                 return $styleFiles;
771         }
772
773         /**
774          * Returns all style files and all skin style files used by this module.
775          *
776          * @return array
777          */
778         public function getAllStyleFiles() {
779                 $collatedStyleFiles = array_merge_recursive(
780                         self::collateFilePathListByOption( $this->styles, 'media', 'all' ),
781                         $this->getAllSkinStyleFiles()
782                 );
783
784                 $result = [];
785
786                 foreach ( $collatedStyleFiles as $media => $styleFiles ) {
787                         foreach ( $styleFiles as $styleFile ) {
788                                 $result[] = $this->getLocalPath( $styleFile );
789                         }
790                 }
791
792                 return $result;
793         }
794
795         /**
796          * Gets the contents of a list of JavaScript files.
797          *
798          * @param array $scripts List of file paths to scripts to read, remap and concetenate
799          * @throws MWException
800          * @return string Concatenated and remapped JavaScript data from $scripts
801          */
802         protected function readScriptFiles( array $scripts ) {
803                 if ( empty( $scripts ) ) {
804                         return '';
805                 }
806                 $js = '';
807                 foreach ( array_unique( $scripts, SORT_REGULAR ) as $fileName ) {
808                         $localPath = $this->getLocalPath( $fileName );
809                         if ( !file_exists( $localPath ) ) {
810                                 throw new MWException( __METHOD__ . ": script file not found: \"$localPath\"" );
811                         }
812                         $contents = $this->stripBom( file_get_contents( $localPath ) );
813                         if ( $this->getConfig()->get( 'ResourceLoaderValidateStaticJS' ) ) {
814                                 // Static files don't really need to be checked as often; unlike
815                                 // on-wiki module they shouldn't change unexpectedly without
816                                 // admin interference.
817                                 $contents = $this->validateScriptFile( $fileName, $contents );
818                         }
819                         $js .= $contents . "\n";
820                 }
821                 return $js;
822         }
823
824         /**
825          * Gets the contents of a list of CSS files.
826          *
827          * @param array $styles List of media type/list of file paths pairs, to read, remap and
828          * concetenate
829          * @param bool $flip
830          * @param ResourceLoaderContext $context
831          *
832          * @throws MWException
833          * @return array List of concatenated and remapped CSS data from $styles,
834          *     keyed by media type
835          *
836          * @since 1.27 Calling this method without a ResourceLoaderContext instance
837          *   is deprecated.
838          */
839         public function readStyleFiles( array $styles, $flip, $context = null ) {
840                 if ( $context === null ) {
841                         wfDeprecated( __METHOD__ . ' without a ResourceLoader context', '1.27' );
842                         $context = ResourceLoaderContext::newDummyContext();
843                 }
844
845                 if ( empty( $styles ) ) {
846                         return [];
847                 }
848                 foreach ( $styles as $media => $files ) {
849                         $uniqueFiles = array_unique( $files, SORT_REGULAR );
850                         $styleFiles = [];
851                         foreach ( $uniqueFiles as $file ) {
852                                 $styleFiles[] = $this->readStyleFile( $file, $flip, $context );
853                         }
854                         $styles[$media] = implode( "\n", $styleFiles );
855                 }
856                 return $styles;
857         }
858
859         /**
860          * Reads a style file.
861          *
862          * This method can be used as a callback for array_map()
863          *
864          * @param string $path File path of style file to read
865          * @param bool $flip
866          * @param ResourceLoaderContext $context
867          *
868          * @return string CSS data in script file
869          * @throws MWException If the file doesn't exist
870          */
871         protected function readStyleFile( $path, $flip, $context ) {
872                 $localPath = $this->getLocalPath( $path );
873                 $remotePath = $this->getRemotePath( $path );
874                 if ( !file_exists( $localPath ) ) {
875                         $msg = __METHOD__ . ": style file not found: \"$localPath\"";
876                         wfDebugLog( 'resourceloader', $msg );
877                         throw new MWException( $msg );
878                 }
879
880                 if ( $this->getStyleSheetLang( $localPath ) === 'less' ) {
881                         $style = $this->compileLessFile( $localPath, $context );
882                         $this->hasGeneratedStyles = true;
883                 } else {
884                         $style = $this->stripBom( file_get_contents( $localPath ) );
885                 }
886
887                 if ( $flip ) {
888                         $style = CSSJanus::transform( $style, true, false );
889                 }
890                 $localDir = dirname( $localPath );
891                 $remoteDir = dirname( $remotePath );
892                 // Get and register local file references
893                 $localFileRefs = CSSMin::getLocalFileReferences( $style, $localDir );
894                 foreach ( $localFileRefs as $file ) {
895                         if ( file_exists( $file ) ) {
896                                 $this->localFileRefs[] = $file;
897                         } else {
898                                 $this->missingLocalFileRefs[] = $file;
899                         }
900                 }
901                 // Don't cache this call. remap() ensures data URIs embeds are up to date,
902                 // and urls contain correct content hashes in their query string. (T128668)
903                 return CSSMin::remap( $style, $localDir, $remoteDir, true );
904         }
905
906         /**
907          * Get whether CSS for this module should be flipped
908          * @param ResourceLoaderContext $context
909          * @return bool
910          */
911         public function getFlip( $context ) {
912                 return $context->getDirection() === 'rtl' && !$this->noflip;
913         }
914
915         /**
916          * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
917          *
918          * @return array Array of strings
919          */
920         public function getTargets() {
921                 return $this->targets;
922         }
923
924         /**
925          * Get the module's load type.
926          *
927          * @since 1.28
928          * @return string
929          */
930         public function getType() {
931                 $canBeStylesOnly = !(
932                         // All options except 'styles', 'skinStyles' and 'debugRaw'
933                         $this->scripts
934                         || $this->debugScripts
935                         || $this->templates
936                         || $this->languageScripts
937                         || $this->skinScripts
938                         || $this->dependencies
939                         || $this->messages
940                         || $this->skipFunction
941                         || $this->raw
942                 );
943                 return $canBeStylesOnly ? self::LOAD_STYLES : self::LOAD_GENERAL;
944         }
945
946         /**
947          * Compile a LESS file into CSS.
948          *
949          * Keeps track of all used files and adds them to localFileRefs.
950          *
951          * @since 1.22
952          * @since 1.27 Added $context paramter.
953          * @throws Exception If less.php encounters a parse error
954          * @param string $fileName File path of LESS source
955          * @param ResourceLoaderContext $context Context in which to generate script
956          * @return string CSS source
957          */
958         protected function compileLessFile( $fileName, ResourceLoaderContext $context ) {
959                 static $cache;
960
961                 if ( !$cache ) {
962                         $cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING );
963                 }
964
965                 // Construct a cache key from the LESS file name and a hash digest
966                 // of the LESS variables used for compilation.
967                 $vars = $this->getLessVars( $context );
968                 ksort( $vars );
969                 $varsHash = hash( 'md4', serialize( $vars ) );
970                 $cacheKey = $cache->makeGlobalKey( 'LESS', $fileName, $varsHash );
971                 $cachedCompile = $cache->get( $cacheKey );
972
973                 // If we got a cached value, we have to validate it by getting a
974                 // checksum of all the files that were loaded by the parser and
975                 // ensuring it matches the cached entry's.
976                 if ( isset( $cachedCompile['hash'] ) ) {
977                         $contentHash = FileContentsHasher::getFileContentsHash( $cachedCompile['files'] );
978                         if ( $contentHash === $cachedCompile['hash'] ) {
979                                 $this->localFileRefs = array_merge( $this->localFileRefs, $cachedCompile['files'] );
980                                 return $cachedCompile['css'];
981                         }
982                 }
983
984                 $compiler = $context->getResourceLoader()->getLessCompiler( $vars );
985                 $css = $compiler->parseFile( $fileName )->getCss();
986                 $files = $compiler->AllParsedFiles();
987                 $this->localFileRefs = array_merge( $this->localFileRefs, $files );
988
989                 // Cache for 24 hours (86400 seconds).
990                 $cache->set( $cacheKey, [
991                         'css'   => $css,
992                         'files' => $files,
993                         'hash'  => FileContentsHasher::getFileContentsHash( $files ),
994                 ], 3600 * 24 );
995
996                 return $css;
997         }
998
999         /**
1000          * Takes named templates by the module and returns an array mapping.
1001          * @return array Templates mapping template alias to content
1002          * @throws MWException
1003          */
1004         public function getTemplates() {
1005                 $templates = [];
1006
1007                 foreach ( $this->templates as $alias => $templatePath ) {
1008                         // Alias is optional
1009                         if ( is_int( $alias ) ) {
1010                                 $alias = $templatePath;
1011                         }
1012                         $localPath = $this->getLocalPath( $templatePath );
1013                         if ( file_exists( $localPath ) ) {
1014                                 $content = file_get_contents( $localPath );
1015                                 $templates[$alias] = $this->stripBom( $content );
1016                         } else {
1017                                 $msg = __METHOD__ . ": template file not found: \"$localPath\"";
1018                                 wfDebugLog( 'resourceloader', $msg );
1019                                 throw new MWException( $msg );
1020                         }
1021                 }
1022                 return $templates;
1023         }
1024
1025         /**
1026          * Takes an input string and removes the UTF-8 BOM character if present
1027          *
1028          * We need to remove these after reading a file, because we concatenate our files and
1029          * the BOM character is not valid in the middle of a string.
1030          * We already assume UTF-8 everywhere, so this should be safe.
1031          *
1032          * @param string $input
1033          * @return string Input minus the intial BOM char
1034          */
1035         protected function stripBom( $input ) {
1036                 if ( substr_compare( "\xef\xbb\xbf", $input, 0, 3 ) === 0 ) {
1037                         return substr( $input, 3 );
1038                 }
1039                 return $input;
1040         }
1041 }