1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* Base class for resource loading system. |
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 Roan Kattouw |
22
|
|
|
* @author Trevor Parscal |
23
|
|
|
*/ |
24
|
|
|
|
25
|
|
|
use Psr\Log\LoggerAwareInterface; |
26
|
|
|
use Psr\Log\LoggerInterface; |
27
|
|
|
use Psr\Log\NullLogger; |
28
|
|
|
use WrappedString\WrappedString; |
29
|
|
|
|
30
|
|
|
/** |
31
|
|
|
* Dynamic JavaScript and CSS resource loading system. |
32
|
|
|
* |
33
|
|
|
* Most of the documentation is on the MediaWiki documentation wiki starting at: |
34
|
|
|
* https://www.mediawiki.org/wiki/ResourceLoader |
35
|
|
|
*/ |
36
|
|
|
class ResourceLoader implements LoggerAwareInterface { |
37
|
|
|
/** @var int */ |
38
|
|
|
protected static $filterCacheVersion = 7; |
39
|
|
|
|
40
|
|
|
/** @var bool */ |
41
|
|
|
protected static $debugMode = null; |
42
|
|
|
|
43
|
|
|
/** @var array */ |
44
|
|
|
private $lessVars = null; |
45
|
|
|
|
46
|
|
|
/** |
47
|
|
|
* Module name/ResourceLoaderModule object pairs |
48
|
|
|
* @var array |
49
|
|
|
*/ |
50
|
|
|
protected $modules = []; |
51
|
|
|
|
52
|
|
|
/** |
53
|
|
|
* Associative array mapping module name to info associative array |
54
|
|
|
* @var array |
55
|
|
|
*/ |
56
|
|
|
protected $moduleInfos = []; |
57
|
|
|
|
58
|
|
|
/** @var Config $config */ |
59
|
|
|
protected $config; |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* Associative array mapping framework ids to a list of names of test suite modules |
63
|
|
|
* like [ 'qunit' => [ 'mediawiki.tests.qunit.suites', 'ext.foo.tests', ... ], ... ] |
64
|
|
|
* @var array |
65
|
|
|
*/ |
66
|
|
|
protected $testModuleNames = []; |
67
|
|
|
|
68
|
|
|
/** |
69
|
|
|
* E.g. [ 'source-id' => 'http://.../load.php' ] |
70
|
|
|
* @var array |
71
|
|
|
*/ |
72
|
|
|
protected $sources = []; |
73
|
|
|
|
74
|
|
|
/** |
75
|
|
|
* Errors accumulated during current respond() call. |
76
|
|
|
* @var array |
77
|
|
|
*/ |
78
|
|
|
protected $errors = []; |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* @var MessageBlobStore |
82
|
|
|
*/ |
83
|
|
|
protected $blobStore; |
84
|
|
|
|
85
|
|
|
/** |
86
|
|
|
* @var LoggerInterface |
87
|
|
|
*/ |
88
|
|
|
private $logger; |
89
|
|
|
|
90
|
|
|
/** @var string JavaScript / CSS pragma to disable minification. **/ |
91
|
|
|
const FILTER_NOMIN = '/*@nomin*/'; |
92
|
|
|
|
93
|
|
|
/** |
94
|
|
|
* Load information stored in the database about modules. |
95
|
|
|
* |
96
|
|
|
* This method grabs modules dependencies from the database and updates modules |
97
|
|
|
* objects. |
98
|
|
|
* |
99
|
|
|
* This is not inside the module code because it is much faster to |
100
|
|
|
* request all of the information at once than it is to have each module |
101
|
|
|
* requests its own information. This sacrifice of modularity yields a substantial |
102
|
|
|
* performance improvement. |
103
|
|
|
* |
104
|
|
|
* @param array $moduleNames List of module names to preload information for |
105
|
|
|
* @param ResourceLoaderContext $context Context to load the information within |
106
|
|
|
*/ |
107
|
|
|
public function preloadModuleInfo( array $moduleNames, ResourceLoaderContext $context ) { |
108
|
|
|
if ( !$moduleNames ) { |
109
|
|
|
// Or else Database*::select() will explode, plus it's cheaper! |
110
|
|
|
return; |
111
|
|
|
} |
112
|
|
|
$dbr = wfGetDB( DB_REPLICA ); |
113
|
|
|
$skin = $context->getSkin(); |
114
|
|
|
$lang = $context->getLanguage(); |
115
|
|
|
|
116
|
|
|
// Batched version of ResourceLoaderModule::getFileDependencies |
117
|
|
|
$vary = "$skin|$lang"; |
118
|
|
|
$res = $dbr->select( 'module_deps', [ 'md_module', 'md_deps' ], [ |
119
|
|
|
'md_module' => $moduleNames, |
120
|
|
|
'md_skin' => $vary, |
121
|
|
|
], __METHOD__ |
122
|
|
|
); |
123
|
|
|
|
124
|
|
|
// Prime in-object cache for file dependencies |
125
|
|
|
$modulesWithDeps = []; |
126
|
|
|
foreach ( $res as $row ) { |
127
|
|
|
$module = $this->getModule( $row->md_module ); |
|
|
|
|
128
|
|
|
if ( $module ) { |
129
|
|
|
$module->setFileDependencies( $context, ResourceLoaderModule::expandRelativePaths( |
130
|
|
|
FormatJson::decode( $row->md_deps, true ) |
131
|
|
|
) ); |
132
|
|
|
$modulesWithDeps[] = $row->md_module; |
133
|
|
|
} |
134
|
|
|
} |
135
|
|
|
// Register the absence of a dependency row too |
136
|
|
|
foreach ( array_diff( $moduleNames, $modulesWithDeps ) as $name ) { |
137
|
|
|
$module = $this->getModule( $name ); |
|
|
|
|
138
|
|
|
if ( $module ) { |
139
|
|
|
$this->getModule( $name )->setFileDependencies( $context, [] ); |
140
|
|
|
} |
141
|
|
|
} |
142
|
|
|
|
143
|
|
|
// Batched version of ResourceLoaderWikiModule::getTitleInfo |
144
|
|
|
ResourceLoaderWikiModule::preloadTitleInfo( $context, $dbr, $moduleNames ); |
|
|
|
|
145
|
|
|
|
146
|
|
|
// Prime in-object cache for message blobs for modules with messages |
147
|
|
|
$modules = []; |
148
|
|
|
foreach ( $moduleNames as $name ) { |
149
|
|
|
$module = $this->getModule( $name ); |
|
|
|
|
150
|
|
|
if ( $module && $module->getMessages() ) { |
151
|
|
|
$modules[$name] = $module; |
152
|
|
|
} |
153
|
|
|
} |
154
|
|
|
$store = $this->getMessageBlobStore(); |
155
|
|
|
$blobs = $store->getBlobs( $modules, $lang ); |
156
|
|
|
foreach ( $blobs as $name => $blob ) { |
157
|
|
|
$modules[$name]->setMessageBlob( $blob, $lang ); |
158
|
|
|
} |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
/** |
162
|
|
|
* Run JavaScript or CSS data through a filter, caching the filtered result for future calls. |
163
|
|
|
* |
164
|
|
|
* Available filters are: |
165
|
|
|
* |
166
|
|
|
* - minify-js \see JavaScriptMinifier::minify |
167
|
|
|
* - minify-css \see CSSMin::minify |
168
|
|
|
* |
169
|
|
|
* If $data is empty, only contains whitespace or the filter was unknown, |
170
|
|
|
* $data is returned unmodified. |
171
|
|
|
* |
172
|
|
|
* @param string $filter Name of filter to run |
173
|
|
|
* @param string $data Text to filter, such as JavaScript or CSS text |
174
|
|
|
* @param array $options Keys: |
175
|
|
|
* - (bool) cache: Whether to allow caching this data. Default: true. |
176
|
|
|
* @return string Filtered data, or a comment containing an error message |
177
|
|
|
*/ |
178
|
|
|
public static function filter( $filter, $data, array $options = [] ) { |
179
|
|
|
if ( strpos( $data, ResourceLoader::FILTER_NOMIN ) !== false ) { |
180
|
|
|
return $data; |
181
|
|
|
} |
182
|
|
|
|
183
|
|
|
if ( isset( $options['cache'] ) && $options['cache'] === false ) { |
184
|
|
|
return self::applyFilter( $filter, $data ); |
185
|
|
|
} |
186
|
|
|
|
187
|
|
|
$stats = RequestContext::getMain()->getStats(); |
188
|
|
|
$cache = ObjectCache::getLocalServerInstance( CACHE_ANYTHING ); |
189
|
|
|
|
190
|
|
|
$key = $cache->makeGlobalKey( |
191
|
|
|
'resourceloader', |
192
|
|
|
'filter', |
193
|
|
|
$filter, |
194
|
|
|
self::$filterCacheVersion, md5( $data ) |
195
|
|
|
); |
196
|
|
|
|
197
|
|
|
$result = $cache->get( $key ); |
198
|
|
|
if ( $result === false ) { |
199
|
|
|
$stats->increment( "resourceloader_cache.$filter.miss" ); |
200
|
|
|
$result = self::applyFilter( $filter, $data ); |
201
|
|
|
$cache->set( $key, $result, 24 * 3600 ); |
202
|
|
|
} else { |
203
|
|
|
$stats->increment( "resourceloader_cache.$filter.hit" ); |
204
|
|
|
} |
205
|
|
|
if ( $result === null ) { |
206
|
|
|
// Cached failure |
207
|
|
|
$result = $data; |
208
|
|
|
} |
209
|
|
|
|
210
|
|
|
return $result; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
private static function applyFilter( $filter, $data ) { |
214
|
|
|
$data = trim( $data ); |
215
|
|
|
if ( $data ) { |
216
|
|
|
try { |
217
|
|
|
$data = ( $filter === 'minify-css' ) |
218
|
|
|
? CSSMin::minify( $data ) |
219
|
|
|
: JavaScriptMinifier::minify( $data ); |
220
|
|
|
} catch ( Exception $e ) { |
221
|
|
|
MWExceptionHandler::logException( $e ); |
222
|
|
|
return null; |
223
|
|
|
} |
224
|
|
|
} |
225
|
|
|
return $data; |
226
|
|
|
} |
227
|
|
|
|
228
|
|
|
/* Methods */ |
229
|
|
|
|
230
|
|
|
/** |
231
|
|
|
* Register core modules and runs registration hooks. |
232
|
|
|
* @param Config $config [optional] |
233
|
|
|
* @param LoggerInterface $logger [optional] |
234
|
|
|
*/ |
235
|
|
|
public function __construct( Config $config = null, LoggerInterface $logger = null ) { |
236
|
|
|
global $IP; |
237
|
|
|
|
238
|
|
|
$this->logger = $logger ?: new NullLogger(); |
239
|
|
|
|
240
|
|
|
if ( !$config ) { |
241
|
|
|
$this->logger->debug( __METHOD__ . ' was called without providing a Config instance' ); |
242
|
|
|
$config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' ); |
243
|
|
|
} |
244
|
|
|
$this->config = $config; |
245
|
|
|
|
246
|
|
|
// Add 'local' source first |
247
|
|
|
$this->addSource( 'local', $config->get( 'LoadScript' ) ); |
248
|
|
|
|
249
|
|
|
// Add other sources |
250
|
|
|
$this->addSource( $config->get( 'ResourceLoaderSources' ) ); |
251
|
|
|
|
252
|
|
|
// Register core modules |
253
|
|
|
$this->register( include "$IP/resources/Resources.php" ); |
254
|
|
|
$this->register( include "$IP/resources/ResourcesOOUI.php" ); |
255
|
|
|
// Register extension modules |
256
|
|
|
$this->register( $config->get( 'ResourceModules' ) ); |
257
|
|
|
Hooks::run( 'ResourceLoaderRegisterModules', [ &$this ] ); |
258
|
|
|
|
259
|
|
|
if ( $config->get( 'EnableJavaScriptTest' ) === true ) { |
260
|
|
|
$this->registerTestModules(); |
261
|
|
|
} |
262
|
|
|
|
263
|
|
|
$this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) ); |
264
|
|
|
} |
265
|
|
|
|
266
|
|
|
/** |
267
|
|
|
* @return Config |
268
|
|
|
*/ |
269
|
|
|
public function getConfig() { |
270
|
|
|
return $this->config; |
271
|
|
|
} |
272
|
|
|
|
273
|
|
|
/** |
274
|
|
|
* @since 1.26 |
275
|
|
|
* @param LoggerInterface $logger |
276
|
|
|
*/ |
277
|
|
|
public function setLogger( LoggerInterface $logger ) { |
278
|
|
|
$this->logger = $logger; |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** |
282
|
|
|
* @since 1.27 |
283
|
|
|
* @return LoggerInterface |
284
|
|
|
*/ |
285
|
|
|
public function getLogger() { |
286
|
|
|
return $this->logger; |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* @since 1.26 |
291
|
|
|
* @return MessageBlobStore |
292
|
|
|
*/ |
293
|
|
|
public function getMessageBlobStore() { |
294
|
|
|
return $this->blobStore; |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
/** |
298
|
|
|
* @since 1.25 |
299
|
|
|
* @param MessageBlobStore $blobStore |
300
|
|
|
*/ |
301
|
|
|
public function setMessageBlobStore( MessageBlobStore $blobStore ) { |
302
|
|
|
$this->blobStore = $blobStore; |
303
|
|
|
} |
304
|
|
|
|
305
|
|
|
/** |
306
|
|
|
* Register a module with the ResourceLoader system. |
307
|
|
|
* |
308
|
|
|
* @param mixed $name Name of module as a string or List of name/object pairs as an array |
309
|
|
|
* @param array $info Module info array. For backwards compatibility with 1.17alpha, |
310
|
|
|
* this may also be a ResourceLoaderModule object. Optional when using |
311
|
|
|
* multiple-registration calling style. |
312
|
|
|
* @throws MWException If a duplicate module registration is attempted |
313
|
|
|
* @throws MWException If a module name contains illegal characters (pipes or commas) |
314
|
|
|
* @throws MWException If something other than a ResourceLoaderModule is being registered |
315
|
|
|
* @return bool False if there were any errors, in which case one or more modules were |
316
|
|
|
* not registered |
317
|
|
|
*/ |
318
|
|
|
public function register( $name, $info = null ) { |
319
|
|
|
$moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' ); |
320
|
|
|
|
321
|
|
|
// Allow multiple modules to be registered in one call |
322
|
|
|
$registrations = is_array( $name ) ? $name : [ $name => $info ]; |
323
|
|
|
foreach ( $registrations as $name => $info ) { |
324
|
|
|
// Warn on duplicate registrations |
325
|
|
|
if ( isset( $this->moduleInfos[$name] ) ) { |
326
|
|
|
// A module has already been registered by this name |
327
|
|
|
$this->logger->warning( |
328
|
|
|
'ResourceLoader duplicate registration warning. ' . |
329
|
|
|
'Another module has already been registered as ' . $name |
330
|
|
|
); |
331
|
|
|
} |
332
|
|
|
|
333
|
|
|
// Check $name for validity |
334
|
|
|
if ( !self::isValidModuleName( $name ) ) { |
335
|
|
|
throw new MWException( "ResourceLoader module name '$name' is invalid, " |
336
|
|
|
. "see ResourceLoader::isValidModuleName()" ); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
// Attach module |
340
|
|
|
if ( $info instanceof ResourceLoaderModule ) { |
341
|
|
|
$this->moduleInfos[$name] = [ 'object' => $info ]; |
342
|
|
|
$info->setName( $name ); |
343
|
|
|
$this->modules[$name] = $info; |
344
|
|
|
} elseif ( is_array( $info ) ) { |
345
|
|
|
// New calling convention |
346
|
|
|
$this->moduleInfos[$name] = $info; |
347
|
|
|
} else { |
348
|
|
|
throw new MWException( |
349
|
|
|
'ResourceLoader module info type error for module \'' . $name . |
350
|
|
|
'\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')' |
351
|
|
|
); |
352
|
|
|
} |
353
|
|
|
|
354
|
|
|
// Last-minute changes |
355
|
|
|
|
356
|
|
|
// Apply custom skin-defined styles to existing modules. |
357
|
|
|
if ( $this->isFileModule( $name ) ) { |
358
|
|
|
foreach ( $moduleSkinStyles as $skinName => $skinStyles ) { |
359
|
|
|
// If this module already defines skinStyles for this skin, ignore $wgResourceModuleSkinStyles. |
360
|
|
|
if ( isset( $this->moduleInfos[$name]['skinStyles'][$skinName] ) ) { |
361
|
|
|
continue; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
// If $name is preceded with a '+', the defined style files will be added to 'default' |
365
|
|
|
// skinStyles, otherwise 'default' will be ignored as it normally would be. |
366
|
|
|
if ( isset( $skinStyles[$name] ) ) { |
367
|
|
|
$paths = (array)$skinStyles[$name]; |
368
|
|
|
$styleFiles = []; |
369
|
|
|
} elseif ( isset( $skinStyles['+' . $name] ) ) { |
370
|
|
|
$paths = (array)$skinStyles['+' . $name]; |
371
|
|
|
$styleFiles = isset( $this->moduleInfos[$name]['skinStyles']['default'] ) ? |
372
|
|
|
(array)$this->moduleInfos[$name]['skinStyles']['default'] : |
373
|
|
|
[]; |
374
|
|
|
} else { |
375
|
|
|
continue; |
376
|
|
|
} |
377
|
|
|
|
378
|
|
|
// Add new file paths, remapping them to refer to our directories and not use settings |
379
|
|
|
// from the module we're modifying, which come from the base definition. |
380
|
|
|
list( $localBasePath, $remoteBasePath ) = |
381
|
|
|
ResourceLoaderFileModule::extractBasePaths( $skinStyles ); |
382
|
|
|
|
383
|
|
|
foreach ( $paths as $path ) { |
384
|
|
|
$styleFiles[] = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath ); |
385
|
|
|
} |
386
|
|
|
|
387
|
|
|
$this->moduleInfos[$name]['skinStyles'][$skinName] = $styleFiles; |
388
|
|
|
} |
389
|
|
|
} |
390
|
|
|
} |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
/** |
394
|
|
|
*/ |
395
|
|
|
public function registerTestModules() { |
396
|
|
|
global $IP; |
397
|
|
|
|
398
|
|
|
if ( $this->config->get( 'EnableJavaScriptTest' ) !== true ) { |
399
|
|
|
throw new MWException( 'Attempt to register JavaScript test modules ' |
400
|
|
|
. 'but <code>$wgEnableJavaScriptTest</code> is false. ' |
401
|
|
|
. 'Edit your <code>LocalSettings.php</code> to enable it.' ); |
402
|
|
|
} |
403
|
|
|
|
404
|
|
|
// Get core test suites |
405
|
|
|
$testModules = []; |
406
|
|
|
$testModules['qunit'] = []; |
407
|
|
|
// Get other test suites (e.g. from extensions) |
408
|
|
|
Hooks::run( 'ResourceLoaderTestModules', [ &$testModules, &$this ] ); |
409
|
|
|
|
410
|
|
|
// Add the testrunner (which configures QUnit) to the dependencies. |
411
|
|
|
// Since it must be ready before any of the test suites are executed. |
412
|
|
|
foreach ( $testModules['qunit'] as &$module ) { |
413
|
|
|
// Make sure all test modules are top-loading so that when QUnit starts |
414
|
|
|
// on document-ready, it will run once and finish. If some tests arrive |
415
|
|
|
// later (possibly after QUnit has already finished) they will be ignored. |
416
|
|
|
$module['position'] = 'top'; |
417
|
|
|
$module['dependencies'][] = 'test.mediawiki.qunit.testrunner'; |
418
|
|
|
} |
419
|
|
|
|
420
|
|
|
$testModules['qunit'] = |
421
|
|
|
( include "$IP/tests/qunit/QUnitTestResources.php" ) + $testModules['qunit']; |
422
|
|
|
|
423
|
|
|
foreach ( $testModules as $id => $names ) { |
424
|
|
|
// Register test modules |
425
|
|
|
$this->register( $testModules[$id] ); |
426
|
|
|
|
427
|
|
|
// Keep track of their names so that they can be loaded together |
428
|
|
|
$this->testModuleNames[$id] = array_keys( $testModules[$id] ); |
429
|
|
|
} |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
/** |
433
|
|
|
* Add a foreign source of modules. |
434
|
|
|
* |
435
|
|
|
* Source IDs are typically the same as the Wiki ID or database name (e.g. lowercase a-z). |
436
|
|
|
* |
437
|
|
|
* @param array|string $id Source ID (string), or [ id1 => loadUrl, id2 => loadUrl, ... ] |
438
|
|
|
* @param string|array $loadUrl load.php url (string), or array with loadUrl key for |
439
|
|
|
* backwards-compatibility. |
440
|
|
|
* @throws MWException |
441
|
|
|
*/ |
442
|
|
|
public function addSource( $id, $loadUrl = null ) { |
443
|
|
|
// Allow multiple sources to be registered in one call |
444
|
|
|
if ( is_array( $id ) ) { |
445
|
|
|
foreach ( $id as $key => $value ) { |
446
|
|
|
$this->addSource( $key, $value ); |
447
|
|
|
} |
448
|
|
|
return; |
449
|
|
|
} |
450
|
|
|
|
451
|
|
|
// Disallow duplicates |
452
|
|
|
if ( isset( $this->sources[$id] ) ) { |
453
|
|
|
throw new MWException( |
454
|
|
|
'ResourceLoader duplicate source addition error. ' . |
455
|
|
|
'Another source has already been registered as ' . $id |
456
|
|
|
); |
457
|
|
|
} |
458
|
|
|
|
459
|
|
|
// Pre 1.24 backwards-compatibility |
460
|
|
|
if ( is_array( $loadUrl ) ) { |
461
|
|
|
if ( !isset( $loadUrl['loadScript'] ) ) { |
462
|
|
|
throw new MWException( |
463
|
|
|
__METHOD__ . ' was passed an array with no "loadScript" key.' |
464
|
|
|
); |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
$loadUrl = $loadUrl['loadScript']; |
468
|
|
|
} |
469
|
|
|
|
470
|
|
|
$this->sources[$id] = $loadUrl; |
471
|
|
|
} |
472
|
|
|
|
473
|
|
|
/** |
474
|
|
|
* Get a list of module names. |
475
|
|
|
* |
476
|
|
|
* @return array List of module names |
477
|
|
|
*/ |
478
|
|
|
public function getModuleNames() { |
479
|
|
|
return array_keys( $this->moduleInfos ); |
480
|
|
|
} |
481
|
|
|
|
482
|
|
|
/** |
483
|
|
|
* Get a list of test module names for one (or all) frameworks. |
484
|
|
|
* |
485
|
|
|
* If the given framework id is unknkown, or if the in-object variable is not an array, |
486
|
|
|
* then it will return an empty array. |
487
|
|
|
* |
488
|
|
|
* @param string $framework Get only the test module names for one |
489
|
|
|
* particular framework (optional) |
490
|
|
|
* @return array |
491
|
|
|
*/ |
492
|
|
|
public function getTestModuleNames( $framework = 'all' ) { |
493
|
|
|
/** @todo api siteinfo prop testmodulenames modulenames */ |
494
|
|
|
if ( $framework == 'all' ) { |
495
|
|
|
return $this->testModuleNames; |
496
|
|
|
} elseif ( isset( $this->testModuleNames[$framework] ) |
497
|
|
|
&& is_array( $this->testModuleNames[$framework] ) |
498
|
|
|
) { |
499
|
|
|
return $this->testModuleNames[$framework]; |
500
|
|
|
} else { |
501
|
|
|
return []; |
502
|
|
|
} |
503
|
|
|
} |
504
|
|
|
|
505
|
|
|
/** |
506
|
|
|
* Check whether a ResourceLoader module is registered |
507
|
|
|
* |
508
|
|
|
* @since 1.25 |
509
|
|
|
* @param string $name |
510
|
|
|
* @return bool |
511
|
|
|
*/ |
512
|
|
|
public function isModuleRegistered( $name ) { |
513
|
|
|
return isset( $this->moduleInfos[$name] ); |
514
|
|
|
} |
515
|
|
|
|
516
|
|
|
/** |
517
|
|
|
* Get the ResourceLoaderModule object for a given module name. |
518
|
|
|
* |
519
|
|
|
* If an array of module parameters exists but a ResourceLoaderModule object has not |
520
|
|
|
* yet been instantiated, this method will instantiate and cache that object such that |
521
|
|
|
* subsequent calls simply return the same object. |
522
|
|
|
* |
523
|
|
|
* @param string $name Module name |
524
|
|
|
* @return ResourceLoaderModule|null If module has been registered, return a |
525
|
|
|
* ResourceLoaderModule instance. Otherwise, return null. |
526
|
|
|
*/ |
527
|
|
|
public function getModule( $name ) { |
528
|
|
|
if ( !isset( $this->modules[$name] ) ) { |
529
|
|
|
if ( !isset( $this->moduleInfos[$name] ) ) { |
530
|
|
|
// No such module |
531
|
|
|
return null; |
532
|
|
|
} |
533
|
|
|
// Construct the requested object |
534
|
|
|
$info = $this->moduleInfos[$name]; |
535
|
|
|
/** @var ResourceLoaderModule $object */ |
536
|
|
|
if ( isset( $info['object'] ) ) { |
537
|
|
|
// Object given in info array |
538
|
|
|
$object = $info['object']; |
539
|
|
|
} else { |
540
|
|
|
if ( !isset( $info['class'] ) ) { |
541
|
|
|
$class = 'ResourceLoaderFileModule'; |
542
|
|
|
} else { |
543
|
|
|
$class = $info['class']; |
544
|
|
|
} |
545
|
|
|
/** @var ResourceLoaderModule $object */ |
546
|
|
|
$object = new $class( $info ); |
547
|
|
|
$object->setConfig( $this->getConfig() ); |
548
|
|
|
$object->setLogger( $this->logger ); |
549
|
|
|
} |
550
|
|
|
$object->setName( $name ); |
551
|
|
|
$this->modules[$name] = $object; |
552
|
|
|
} |
553
|
|
|
|
554
|
|
|
return $this->modules[$name]; |
555
|
|
|
} |
556
|
|
|
|
557
|
|
|
/** |
558
|
|
|
* Return whether the definition of a module corresponds to a simple ResourceLoaderFileModule. |
559
|
|
|
* |
560
|
|
|
* @param string $name Module name |
561
|
|
|
* @return bool |
562
|
|
|
*/ |
563
|
|
|
protected function isFileModule( $name ) { |
564
|
|
|
if ( !isset( $this->moduleInfos[$name] ) ) { |
565
|
|
|
return false; |
566
|
|
|
} |
567
|
|
|
$info = $this->moduleInfos[$name]; |
568
|
|
|
if ( isset( $info['object'] ) || isset( $info['class'] ) ) { |
569
|
|
|
return false; |
570
|
|
|
} |
571
|
|
|
return true; |
572
|
|
|
} |
573
|
|
|
|
574
|
|
|
/** |
575
|
|
|
* Get the list of sources. |
576
|
|
|
* |
577
|
|
|
* @return array Like [ id => load.php url, ... ] |
578
|
|
|
*/ |
579
|
|
|
public function getSources() { |
580
|
|
|
return $this->sources; |
581
|
|
|
} |
582
|
|
|
|
583
|
|
|
/** |
584
|
|
|
* Get the URL to the load.php endpoint for the given |
585
|
|
|
* ResourceLoader source |
586
|
|
|
* |
587
|
|
|
* @since 1.24 |
588
|
|
|
* @param string $source |
589
|
|
|
* @throws MWException On an invalid $source name |
590
|
|
|
* @return string |
591
|
|
|
*/ |
592
|
|
|
public function getLoadScript( $source ) { |
593
|
|
|
if ( !isset( $this->sources[$source] ) ) { |
594
|
|
|
throw new MWException( "The $source source was never registered in ResourceLoader." ); |
595
|
|
|
} |
596
|
|
|
return $this->sources[$source]; |
597
|
|
|
} |
598
|
|
|
|
599
|
|
|
/** |
600
|
|
|
* @since 1.26 |
601
|
|
|
* @param string $value |
602
|
|
|
* @return string Hash |
603
|
|
|
*/ |
604
|
|
|
public static function makeHash( $value ) { |
605
|
|
|
$hash = hash( 'fnv132', $value ); |
606
|
|
|
return Wikimedia\base_convert( $hash, 16, 36, 7 ); |
607
|
|
|
} |
608
|
|
|
|
609
|
|
|
/** |
610
|
|
|
* Helper method to get and combine versions of multiple modules. |
611
|
|
|
* |
612
|
|
|
* @since 1.26 |
613
|
|
|
* @param ResourceLoaderContext $context |
614
|
|
|
* @param string[] $modules List of known module names |
|
|
|
|
615
|
|
|
* @return string Hash |
616
|
|
|
*/ |
617
|
|
|
public function getCombinedVersion( ResourceLoaderContext $context, array $moduleNames ) { |
618
|
|
|
if ( !$moduleNames ) { |
619
|
|
|
return ''; |
620
|
|
|
} |
621
|
|
|
$hashes = array_map( function ( $module ) use ( $context ) { |
622
|
|
|
return $this->getModule( $module )->getVersionHash( $context ); |
623
|
|
|
}, $moduleNames ); |
624
|
|
|
return self::makeHash( implode( '', $hashes ) ); |
625
|
|
|
} |
626
|
|
|
|
627
|
|
|
/** |
628
|
|
|
* Get the expected value of the 'version' query parameter. |
629
|
|
|
* |
630
|
|
|
* This is used by respond() to set a short Cache-Control header for requests with |
631
|
|
|
* information newer than the current server has. This avoids pollution of edge caches. |
632
|
|
|
* Typically during deployment. (T117587) |
633
|
|
|
* |
634
|
|
|
* This MUST match return value of `mw.loader#getCombinedVersion()` client-side. |
635
|
|
|
* |
636
|
|
|
* @since 1.28 |
637
|
|
|
* @param ResourceLoaderContext $context |
638
|
|
|
* @param string[] $modules List of module names |
|
|
|
|
639
|
|
|
* @return string Hash |
640
|
|
|
*/ |
641
|
|
|
public function makeVersionQuery( ResourceLoaderContext $context ) { |
642
|
|
|
// As of MediaWiki 1.28, the server and client use the same algorithm for combining |
643
|
|
|
// version hashes. There is no technical reason for this to be same, and for years the |
644
|
|
|
// implementations differed. If getCombinedVersion in PHP (used for StartupModule and |
645
|
|
|
// E-Tag headers) differs in the future from getCombinedVersion in JS (used for 'version' |
646
|
|
|
// query parameter), then this method must continue to match the JS one. |
647
|
|
|
$moduleNames = []; |
648
|
|
|
foreach ( $context->getModules() as $name ) { |
649
|
|
|
if ( !$this->getModule( $name ) ) { |
650
|
|
|
// If a versioned request contains a missing module, the version is a mismatch |
651
|
|
|
// as the client considered a module (and version) we don't have. |
652
|
|
|
return ''; |
653
|
|
|
} |
654
|
|
|
$moduleNames[] = $name; |
655
|
|
|
} |
656
|
|
|
return $this->getCombinedVersion( $context, $moduleNames ); |
657
|
|
|
} |
658
|
|
|
|
659
|
|
|
/** |
660
|
|
|
* Output a response to a load request, including the content-type header. |
661
|
|
|
* |
662
|
|
|
* @param ResourceLoaderContext $context Context in which a response should be formed |
663
|
|
|
*/ |
664
|
|
|
public function respond( ResourceLoaderContext $context ) { |
665
|
|
|
// Buffer output to catch warnings. Normally we'd use ob_clean() on the |
666
|
|
|
// top-level output buffer to clear warnings, but that breaks when ob_gzhandler |
667
|
|
|
// is used: ob_clean() will clear the GZIP header in that case and it won't come |
668
|
|
|
// back for subsequent output, resulting in invalid GZIP. So we have to wrap |
669
|
|
|
// the whole thing in our own output buffer to be sure the active buffer |
670
|
|
|
// doesn't use ob_gzhandler. |
671
|
|
|
// See https://bugs.php.net/bug.php?id=36514 |
672
|
|
|
ob_start(); |
673
|
|
|
|
674
|
|
|
// Find out which modules are missing and instantiate the others |
675
|
|
|
$modules = []; |
676
|
|
|
$missing = []; |
677
|
|
|
foreach ( $context->getModules() as $name ) { |
678
|
|
|
$module = $this->getModule( $name ); |
679
|
|
|
if ( $module ) { |
680
|
|
|
// Do not allow private modules to be loaded from the web. |
681
|
|
|
// This is a security issue, see bug 34907. |
682
|
|
|
if ( $module->getGroup() === 'private' ) { |
683
|
|
|
$this->logger->debug( "Request for private module '$name' denied" ); |
684
|
|
|
$this->errors[] = "Cannot show private module \"$name\""; |
685
|
|
|
continue; |
686
|
|
|
} |
687
|
|
|
$modules[$name] = $module; |
688
|
|
|
} else { |
689
|
|
|
$missing[] = $name; |
690
|
|
|
} |
691
|
|
|
} |
692
|
|
|
|
693
|
|
|
try { |
694
|
|
|
// Preload for getCombinedVersion() and for batch makeModuleResponse() |
695
|
|
|
$this->preloadModuleInfo( array_keys( $modules ), $context ); |
696
|
|
|
} catch ( Exception $e ) { |
697
|
|
|
MWExceptionHandler::logException( $e ); |
698
|
|
|
$this->logger->warning( 'Preloading module info failed: {exception}', [ |
699
|
|
|
'exception' => $e |
700
|
|
|
] ); |
701
|
|
|
$this->errors[] = self::formatExceptionNoComment( $e ); |
702
|
|
|
} |
703
|
|
|
|
704
|
|
|
// Combine versions to propagate cache invalidation |
705
|
|
|
$versionHash = ''; |
706
|
|
|
try { |
707
|
|
|
$versionHash = $this->getCombinedVersion( $context, array_keys( $modules ) ); |
708
|
|
|
} catch ( Exception $e ) { |
709
|
|
|
MWExceptionHandler::logException( $e ); |
710
|
|
|
$this->logger->warning( 'Calculating version hash failed: {exception}', [ |
711
|
|
|
'exception' => $e |
712
|
|
|
] ); |
713
|
|
|
$this->errors[] = self::formatExceptionNoComment( $e ); |
714
|
|
|
} |
715
|
|
|
|
716
|
|
|
// See RFC 2616 § 3.11 Entity Tags |
717
|
|
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11 |
718
|
|
|
$etag = 'W/"' . $versionHash . '"'; |
719
|
|
|
|
720
|
|
|
// Try the client-side cache first |
721
|
|
|
if ( $this->tryRespondNotModified( $context, $etag ) ) { |
722
|
|
|
return; // output handled (buffers cleared) |
723
|
|
|
} |
724
|
|
|
|
725
|
|
|
// Use file cache if enabled and available... |
726
|
|
|
if ( $this->config->get( 'UseFileCache' ) ) { |
727
|
|
|
$fileCache = ResourceFileCache::newFromContext( $context ); |
728
|
|
|
if ( $this->tryRespondFromFileCache( $fileCache, $context, $etag ) ) { |
729
|
|
|
return; // output handled |
730
|
|
|
} |
731
|
|
|
} |
732
|
|
|
|
733
|
|
|
// Generate a response |
734
|
|
|
$response = $this->makeModuleResponse( $context, $modules, $missing ); |
735
|
|
|
|
736
|
|
|
// Capture any PHP warnings from the output buffer and append them to the |
737
|
|
|
// error list if we're in debug mode. |
738
|
|
|
if ( $context->getDebug() ) { |
739
|
|
|
$warnings = ob_get_contents(); |
740
|
|
|
if ( strlen( $warnings ) ) { |
741
|
|
|
$this->errors[] = $warnings; |
742
|
|
|
} |
743
|
|
|
} |
744
|
|
|
|
745
|
|
|
// Save response to file cache unless there are errors |
746
|
|
|
if ( isset( $fileCache ) && !$this->errors && !count( $missing ) ) { |
747
|
|
|
// Cache single modules and images...and other requests if there are enough hits |
748
|
|
|
if ( ResourceFileCache::useFileCache( $context ) ) { |
749
|
|
|
if ( $fileCache->isCacheWorthy() ) { |
750
|
|
|
$fileCache->saveText( $response ); |
751
|
|
|
} else { |
752
|
|
|
$fileCache->incrMissesRecent( $context->getRequest() ); |
753
|
|
|
} |
754
|
|
|
} |
755
|
|
|
} |
756
|
|
|
|
757
|
|
|
$this->sendResponseHeaders( $context, $etag, (bool)$this->errors ); |
758
|
|
|
|
759
|
|
|
// Remove the output buffer and output the response |
760
|
|
|
ob_end_clean(); |
761
|
|
|
|
762
|
|
|
if ( $context->getImageObj() && $this->errors ) { |
763
|
|
|
// We can't show both the error messages and the response when it's an image. |
764
|
|
|
$response = implode( "\n\n", $this->errors ); |
765
|
|
|
} elseif ( $this->errors ) { |
766
|
|
|
$errorText = implode( "\n\n", $this->errors ); |
767
|
|
|
$errorResponse = self::makeComment( $errorText ); |
768
|
|
|
if ( $context->shouldIncludeScripts() ) { |
769
|
|
|
$errorResponse .= 'if (window.console && console.error) {' |
770
|
|
|
. Xml::encodeJsCall( 'console.error', [ $errorText ] ) |
771
|
|
|
. "}\n"; |
772
|
|
|
} |
773
|
|
|
|
774
|
|
|
// Prepend error info to the response |
775
|
|
|
$response = $errorResponse . $response; |
776
|
|
|
} |
777
|
|
|
|
778
|
|
|
$this->errors = []; |
779
|
|
|
echo $response; |
780
|
|
|
} |
781
|
|
|
|
782
|
|
|
/** |
783
|
|
|
* Send main response headers to the client. |
784
|
|
|
* |
785
|
|
|
* Deals with Content-Type, CORS (for stylesheets), and caching. |
786
|
|
|
* |
787
|
|
|
* @param ResourceLoaderContext $context |
788
|
|
|
* @param string $etag ETag header value |
789
|
|
|
* @param bool $errors Whether there are errors in the response |
790
|
|
|
* @return void |
791
|
|
|
*/ |
792
|
|
|
protected function sendResponseHeaders( ResourceLoaderContext $context, $etag, $errors ) { |
793
|
|
|
$rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); |
794
|
|
|
// Use a short cache expiry so that updates propagate to clients quickly, if: |
795
|
|
|
// - No version specified (shared resources, e.g. stylesheets) |
796
|
|
|
// - There were errors (recover quickly) |
797
|
|
|
// - Version mismatch (T117587, T47877) |
798
|
|
|
if ( is_null( $context->getVersion() ) |
799
|
|
|
|| $errors |
800
|
|
|
|| $context->getVersion() !== $this->makeVersionQuery( $context ) |
801
|
|
|
) { |
802
|
|
|
$maxage = $rlMaxage['unversioned']['client']; |
803
|
|
|
$smaxage = $rlMaxage['unversioned']['server']; |
804
|
|
|
// If a version was specified we can use a longer expiry time since changing |
805
|
|
|
// version numbers causes cache misses |
806
|
|
|
} else { |
807
|
|
|
$maxage = $rlMaxage['versioned']['client']; |
808
|
|
|
$smaxage = $rlMaxage['versioned']['server']; |
809
|
|
|
} |
810
|
|
|
if ( $context->getImageObj() ) { |
811
|
|
|
// Output different headers if we're outputting textual errors. |
812
|
|
|
if ( $errors ) { |
813
|
|
|
header( 'Content-Type: text/plain; charset=utf-8' ); |
814
|
|
|
} else { |
815
|
|
|
$context->getImageObj()->sendResponseHeaders( $context ); |
816
|
|
|
} |
817
|
|
|
} elseif ( $context->getOnly() === 'styles' ) { |
818
|
|
|
header( 'Content-Type: text/css; charset=utf-8' ); |
819
|
|
|
header( 'Access-Control-Allow-Origin: *' ); |
820
|
|
|
} else { |
821
|
|
|
header( 'Content-Type: text/javascript; charset=utf-8' ); |
822
|
|
|
} |
823
|
|
|
// See RFC 2616 § 14.19 ETag |
824
|
|
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19 |
825
|
|
|
header( 'ETag: ' . $etag ); |
826
|
|
|
if ( $context->getDebug() ) { |
827
|
|
|
// Do not cache debug responses |
828
|
|
|
header( 'Cache-Control: private, no-cache, must-revalidate' ); |
829
|
|
|
header( 'Pragma: no-cache' ); |
830
|
|
|
} else { |
831
|
|
|
header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" ); |
832
|
|
|
$exp = min( $maxage, $smaxage ); |
833
|
|
|
header( 'Expires: ' . wfTimestamp( TS_RFC2822, $exp + time() ) ); |
834
|
|
|
} |
835
|
|
|
} |
836
|
|
|
|
837
|
|
|
/** |
838
|
|
|
* Respond with HTTP 304 Not Modified if appropiate. |
839
|
|
|
* |
840
|
|
|
* If there's an If-None-Match header, respond with a 304 appropriately |
841
|
|
|
* and clear out the output buffer. If the client cache is too old then do nothing. |
842
|
|
|
* |
843
|
|
|
* @param ResourceLoaderContext $context |
844
|
|
|
* @param string $etag ETag header value |
845
|
|
|
* @return bool True if HTTP 304 was sent and output handled |
846
|
|
|
*/ |
847
|
|
|
protected function tryRespondNotModified( ResourceLoaderContext $context, $etag ) { |
848
|
|
|
// See RFC 2616 § 14.26 If-None-Match |
849
|
|
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26 |
850
|
|
|
$clientKeys = $context->getRequest()->getHeader( 'If-None-Match', WebRequest::GETHEADER_LIST ); |
851
|
|
|
// Never send 304s in debug mode |
852
|
|
|
if ( $clientKeys !== false && !$context->getDebug() && in_array( $etag, $clientKeys ) ) { |
853
|
|
|
// There's another bug in ob_gzhandler (see also the comment at |
854
|
|
|
// the top of this function) that causes it to gzip even empty |
855
|
|
|
// responses, meaning it's impossible to produce a truly empty |
856
|
|
|
// response (because the gzip header is always there). This is |
857
|
|
|
// a problem because 304 responses have to be completely empty |
858
|
|
|
// per the HTTP spec, and Firefox behaves buggily when they're not. |
859
|
|
|
// See also https://bugs.php.net/bug.php?id=51579 |
860
|
|
|
// To work around this, we tear down all output buffering before |
861
|
|
|
// sending the 304. |
862
|
|
|
wfResetOutputBuffers( /* $resetGzipEncoding = */ true ); |
863
|
|
|
|
864
|
|
|
HttpStatus::header( 304 ); |
865
|
|
|
|
866
|
|
|
$this->sendResponseHeaders( $context, $etag, false ); |
867
|
|
|
return true; |
868
|
|
|
} |
869
|
|
|
return false; |
870
|
|
|
} |
871
|
|
|
|
872
|
|
|
/** |
873
|
|
|
* Send out code for a response from file cache if possible. |
874
|
|
|
* |
875
|
|
|
* @param ResourceFileCache $fileCache Cache object for this request URL |
876
|
|
|
* @param ResourceLoaderContext $context Context in which to generate a response |
877
|
|
|
* @param string $etag ETag header value |
878
|
|
|
* @return bool If this found a cache file and handled the response |
879
|
|
|
*/ |
880
|
|
|
protected function tryRespondFromFileCache( |
881
|
|
|
ResourceFileCache $fileCache, |
882
|
|
|
ResourceLoaderContext $context, |
883
|
|
|
$etag |
884
|
|
|
) { |
885
|
|
|
$rlMaxage = $this->config->get( 'ResourceLoaderMaxage' ); |
886
|
|
|
// Buffer output to catch warnings. |
887
|
|
|
ob_start(); |
888
|
|
|
// Get the maximum age the cache can be |
889
|
|
|
$maxage = is_null( $context->getVersion() ) |
890
|
|
|
? $rlMaxage['unversioned']['server'] |
891
|
|
|
: $rlMaxage['versioned']['server']; |
892
|
|
|
// Minimum timestamp the cache file must have |
893
|
|
|
$good = $fileCache->isCacheGood( wfTimestamp( TS_MW, time() - $maxage ) ); |
|
|
|
|
894
|
|
|
if ( !$good ) { |
895
|
|
|
try { // RL always hits the DB on file cache miss... |
896
|
|
|
wfGetDB( DB_REPLICA ); |
897
|
|
|
} catch ( DBConnectionError $e ) { // ...check if we need to fallback to cache |
898
|
|
|
$good = $fileCache->isCacheGood(); // cache existence check |
899
|
|
|
} |
900
|
|
|
} |
901
|
|
|
if ( $good ) { |
902
|
|
|
$ts = $fileCache->cacheTimestamp(); |
903
|
|
|
// Send content type and cache headers |
904
|
|
|
$this->sendResponseHeaders( $context, $etag, false ); |
905
|
|
|
$response = $fileCache->fetchText(); |
906
|
|
|
// Capture any PHP warnings from the output buffer and append them to the |
907
|
|
|
// response in a comment if we're in debug mode. |
908
|
|
|
if ( $context->getDebug() ) { |
909
|
|
|
$warnings = ob_get_contents(); |
910
|
|
|
if ( strlen( $warnings ) ) { |
911
|
|
|
$response = self::makeComment( $warnings ) . $response; |
912
|
|
|
} |
913
|
|
|
} |
914
|
|
|
// Remove the output buffer and output the response |
915
|
|
|
ob_end_clean(); |
916
|
|
|
echo $response . "\n/* Cached {$ts} */"; |
917
|
|
|
return true; // cache hit |
918
|
|
|
} |
919
|
|
|
// Clear buffer |
920
|
|
|
ob_end_clean(); |
921
|
|
|
|
922
|
|
|
return false; // cache miss |
923
|
|
|
} |
924
|
|
|
|
925
|
|
|
/** |
926
|
|
|
* Generate a CSS or JS comment block. |
927
|
|
|
* |
928
|
|
|
* Only use this for public data, not error message details. |
929
|
|
|
* |
930
|
|
|
* @param string $text |
931
|
|
|
* @return string |
932
|
|
|
*/ |
933
|
|
|
public static function makeComment( $text ) { |
934
|
|
|
$encText = str_replace( '*/', '* /', $text ); |
935
|
|
|
return "/*\n$encText\n*/\n"; |
936
|
|
|
} |
937
|
|
|
|
938
|
|
|
/** |
939
|
|
|
* Handle exception display. |
940
|
|
|
* |
941
|
|
|
* @param Exception $e Exception to be shown to the user |
942
|
|
|
* @return string Sanitized text in a CSS/JS comment that can be returned to the user |
943
|
|
|
*/ |
944
|
|
|
public static function formatException( $e ) { |
945
|
|
|
return self::makeComment( self::formatExceptionNoComment( $e ) ); |
946
|
|
|
} |
947
|
|
|
|
948
|
|
|
/** |
949
|
|
|
* Handle exception display. |
950
|
|
|
* |
951
|
|
|
* @since 1.25 |
952
|
|
|
* @param Exception $e Exception to be shown to the user |
953
|
|
|
* @return string Sanitized text that can be returned to the user |
954
|
|
|
*/ |
955
|
|
|
protected static function formatExceptionNoComment( $e ) { |
956
|
|
|
global $wgShowExceptionDetails; |
957
|
|
|
|
958
|
|
|
if ( !$wgShowExceptionDetails ) { |
959
|
|
|
return MWExceptionHandler::getPublicLogMessage( $e ); |
960
|
|
|
} |
961
|
|
|
|
962
|
|
|
return MWExceptionHandler::getLogMessage( $e ); |
963
|
|
|
} |
964
|
|
|
|
965
|
|
|
/** |
966
|
|
|
* Generate code for a response. |
967
|
|
|
* |
968
|
|
|
* @param ResourceLoaderContext $context Context in which to generate a response |
969
|
|
|
* @param ResourceLoaderModule[] $modules List of module objects keyed by module name |
970
|
|
|
* @param string[] $missing List of requested module names that are unregistered (optional) |
971
|
|
|
* @return string Response data |
972
|
|
|
*/ |
973
|
|
|
public function makeModuleResponse( ResourceLoaderContext $context, |
974
|
|
|
array $modules, array $missing = [] |
975
|
|
|
) { |
976
|
|
|
$out = ''; |
977
|
|
|
$states = []; |
978
|
|
|
|
979
|
|
|
if ( !count( $modules ) && !count( $missing ) ) { |
980
|
|
|
return <<<MESSAGE |
981
|
|
|
/* This file is the Web entry point for MediaWiki's ResourceLoader: |
982
|
|
|
<https://www.mediawiki.org/wiki/ResourceLoader>. In this request, |
983
|
|
|
no modules were requested. Max made me put this here. */ |
984
|
|
|
MESSAGE; |
985
|
|
|
} |
986
|
|
|
|
987
|
|
|
$image = $context->getImageObj(); |
988
|
|
|
if ( $image ) { |
989
|
|
|
$data = $image->getImageData( $context ); |
990
|
|
|
if ( $data === false ) { |
991
|
|
|
$data = ''; |
992
|
|
|
$this->errors[] = 'Image generation failed'; |
993
|
|
|
} |
994
|
|
|
return $data; |
995
|
|
|
} |
996
|
|
|
|
997
|
|
|
foreach ( $missing as $name ) { |
998
|
|
|
$states[$name] = 'missing'; |
999
|
|
|
} |
1000
|
|
|
|
1001
|
|
|
// Generate output |
1002
|
|
|
$isRaw = false; |
1003
|
|
|
|
1004
|
|
|
$filter = $context->getOnly() === 'styles' ? 'minify-css' : 'minify-js'; |
1005
|
|
|
|
1006
|
|
|
foreach ( $modules as $name => $module ) { |
1007
|
|
|
try { |
1008
|
|
|
$content = $module->getModuleContent( $context ); |
1009
|
|
|
$implementKey = $name . '@' . $module->getVersionHash( $context ); |
1010
|
|
|
$strContent = ''; |
1011
|
|
|
|
1012
|
|
|
// Append output |
1013
|
|
|
switch ( $context->getOnly() ) { |
1014
|
|
|
case 'scripts': |
1015
|
|
|
$scripts = $content['scripts']; |
1016
|
|
|
if ( is_string( $scripts ) ) { |
1017
|
|
|
// Load scripts raw... |
1018
|
|
|
$strContent = $scripts; |
1019
|
|
|
} elseif ( is_array( $scripts ) ) { |
1020
|
|
|
// ...except when $scripts is an array of URLs |
1021
|
|
|
$strContent = self::makeLoaderImplementScript( $implementKey, $scripts, [], [], [] ); |
1022
|
|
|
} |
1023
|
|
|
break; |
1024
|
|
|
case 'styles': |
1025
|
|
|
$styles = $content['styles']; |
1026
|
|
|
// We no longer seperate into media, they are all combined now with |
1027
|
|
|
// custom media type groups into @media .. {} sections as part of the css string. |
1028
|
|
|
// Module returns either an empty array or a numerical array with css strings. |
1029
|
|
|
$strContent = isset( $styles['css'] ) ? implode( '', $styles['css'] ) : ''; |
1030
|
|
|
break; |
1031
|
|
|
default: |
1032
|
|
|
$scripts = isset( $content['scripts'] ) ? $content['scripts'] : ''; |
1033
|
|
|
if ( is_string( $scripts ) ) { |
1034
|
|
|
if ( $name === 'site' || $name === 'user' ) { |
|
|
|
|
1035
|
|
|
// Legacy scripts that run in the global scope without a closure. |
1036
|
|
|
// mw.loader.implement will use globalEval if scripts is a string. |
1037
|
|
|
// Minify manually here, because general response minification is |
1038
|
|
|
// not effective due it being a string literal, not a function. |
1039
|
|
|
if ( !ResourceLoader::inDebugMode() ) { |
1040
|
|
|
$scripts = self::filter( 'minify-js', $scripts ); // T107377 |
1041
|
|
|
} |
1042
|
|
|
} else { |
1043
|
|
|
$scripts = new XmlJsCode( $scripts ); |
1044
|
|
|
} |
1045
|
|
|
} |
1046
|
|
|
$strContent = self::makeLoaderImplementScript( |
1047
|
|
|
$implementKey, |
1048
|
|
|
$scripts, |
1049
|
|
|
isset( $content['styles'] ) ? $content['styles'] : [], |
1050
|
|
|
isset( $content['messagesBlob'] ) ? new XmlJsCode( $content['messagesBlob'] ) : [], |
1051
|
|
|
isset( $content['templates'] ) ? $content['templates'] : [] |
1052
|
|
|
); |
1053
|
|
|
break; |
1054
|
|
|
} |
1055
|
|
|
|
1056
|
|
|
if ( !$context->getDebug() ) { |
1057
|
|
|
$strContent = self::filter( $filter, $strContent ); |
|
|
|
|
1058
|
|
|
} |
1059
|
|
|
|
1060
|
|
|
$out .= $strContent; |
1061
|
|
|
|
1062
|
|
|
} catch ( Exception $e ) { |
1063
|
|
|
MWExceptionHandler::logException( $e ); |
1064
|
|
|
$this->logger->warning( 'Generating module package failed: {exception}', [ |
1065
|
|
|
'exception' => $e |
1066
|
|
|
] ); |
1067
|
|
|
$this->errors[] = self::formatExceptionNoComment( $e ); |
1068
|
|
|
|
1069
|
|
|
// Respond to client with error-state instead of module implementation |
1070
|
|
|
$states[$name] = 'error'; |
1071
|
|
|
unset( $modules[$name] ); |
1072
|
|
|
} |
1073
|
|
|
$isRaw |= $module->isRaw(); |
1074
|
|
|
} |
1075
|
|
|
|
1076
|
|
|
// Update module states |
1077
|
|
|
if ( $context->shouldIncludeScripts() && !$context->getRaw() && !$isRaw ) { |
1078
|
|
|
if ( count( $modules ) && $context->getOnly() === 'scripts' ) { |
1079
|
|
|
// Set the state of modules loaded as only scripts to ready as |
1080
|
|
|
// they don't have an mw.loader.implement wrapper that sets the state |
1081
|
|
|
foreach ( $modules as $name => $module ) { |
1082
|
|
|
$states[$name] = 'ready'; |
1083
|
|
|
} |
1084
|
|
|
} |
1085
|
|
|
|
1086
|
|
|
// Set the state of modules we didn't respond to with mw.loader.implement |
1087
|
|
|
if ( count( $states ) ) { |
1088
|
|
|
$stateScript = self::makeLoaderStateScript( $states ); |
1089
|
|
|
if ( !$context->getDebug() ) { |
1090
|
|
|
$stateScript = self::filter( 'minify-js', $stateScript ); |
|
|
|
|
1091
|
|
|
} |
1092
|
|
|
$out .= $stateScript; |
1093
|
|
|
} |
1094
|
|
|
} else { |
1095
|
|
|
if ( count( $states ) ) { |
1096
|
|
|
$this->errors[] = 'Problematic modules: ' . |
1097
|
|
|
FormatJson::encode( $states, ResourceLoader::inDebugMode() ); |
1098
|
|
|
} |
1099
|
|
|
} |
1100
|
|
|
|
1101
|
|
|
return $out; |
1102
|
|
|
} |
1103
|
|
|
|
1104
|
|
|
/** |
1105
|
|
|
* Get names of modules that use a certain message. |
1106
|
|
|
* |
1107
|
|
|
* @param string $messageKey |
1108
|
|
|
* @return array List of module names |
1109
|
|
|
*/ |
1110
|
|
|
public function getModulesByMessage( $messageKey ) { |
1111
|
|
|
$moduleNames = []; |
1112
|
|
|
foreach ( $this->getModuleNames() as $moduleName ) { |
1113
|
|
|
$module = $this->getModule( $moduleName ); |
1114
|
|
|
if ( in_array( $messageKey, $module->getMessages() ) ) { |
1115
|
|
|
$moduleNames[] = $moduleName; |
1116
|
|
|
} |
1117
|
|
|
} |
1118
|
|
|
return $moduleNames; |
1119
|
|
|
} |
1120
|
|
|
|
1121
|
|
|
/* Static Methods */ |
1122
|
|
|
|
1123
|
|
|
/** |
1124
|
|
|
* Return JS code that calls mw.loader.implement with given module properties. |
1125
|
|
|
* |
1126
|
|
|
* @param string $name Module name or implement key (format "`[name]@[version]`") |
1127
|
|
|
* @param XmlJsCode|array|string $scripts Code as XmlJsCode (to be wrapped in a closure), |
1128
|
|
|
* list of URLs to JavaScript files, or a string of JavaScript for `$.globalEval`. |
1129
|
|
|
* @param mixed $styles Array of CSS strings keyed by media type, or an array of lists of URLs |
1130
|
|
|
* to CSS files keyed by media type |
1131
|
|
|
* @param mixed $messages List of messages associated with this module. May either be an |
1132
|
|
|
* associative array mapping message key to value, or a JSON-encoded message blob containing |
1133
|
|
|
* the same data, wrapped in an XmlJsCode object. |
1134
|
|
|
* @param array $templates Keys are name of templates and values are the source of |
1135
|
|
|
* the template. |
1136
|
|
|
* @throws MWException |
1137
|
|
|
* @return string |
1138
|
|
|
*/ |
1139
|
|
|
protected static function makeLoaderImplementScript( |
1140
|
|
|
$name, $scripts, $styles, $messages, $templates |
1141
|
|
|
) { |
1142
|
|
|
if ( $scripts instanceof XmlJsCode ) { |
1143
|
|
|
$scripts = new XmlJsCode( "function ( $, jQuery, require, module ) {\n{$scripts->value}\n}" ); |
1144
|
|
|
} elseif ( !is_string( $scripts ) && !is_array( $scripts ) ) { |
1145
|
|
|
throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' ); |
1146
|
|
|
} |
1147
|
|
|
// mw.loader.implement requires 'styles', 'messages' and 'templates' to be objects (not |
1148
|
|
|
// arrays). json_encode considers empty arrays to be numerical and outputs "[]" instead |
1149
|
|
|
// of "{}". Force them to objects. |
1150
|
|
|
$module = [ |
1151
|
|
|
$name, |
1152
|
|
|
$scripts, |
1153
|
|
|
(object)$styles, |
1154
|
|
|
(object)$messages, |
1155
|
|
|
(object)$templates, |
1156
|
|
|
]; |
1157
|
|
|
self::trimArray( $module ); |
1158
|
|
|
|
1159
|
|
|
return Xml::encodeJsCall( 'mw.loader.implement', $module, ResourceLoader::inDebugMode() ); |
1160
|
|
|
} |
1161
|
|
|
|
1162
|
|
|
/** |
1163
|
|
|
* Returns JS code which, when called, will register a given list of messages. |
1164
|
|
|
* |
1165
|
|
|
* @param mixed $messages Either an associative array mapping message key to value, or a |
1166
|
|
|
* JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object. |
1167
|
|
|
* @return string |
1168
|
|
|
*/ |
1169
|
|
|
public static function makeMessageSetScript( $messages ) { |
1170
|
|
|
return Xml::encodeJsCall( |
1171
|
|
|
'mw.messages.set', |
1172
|
|
|
[ (object)$messages ], |
1173
|
|
|
ResourceLoader::inDebugMode() |
1174
|
|
|
); |
1175
|
|
|
} |
1176
|
|
|
|
1177
|
|
|
/** |
1178
|
|
|
* Combines an associative array mapping media type to CSS into a |
1179
|
|
|
* single stylesheet with "@media" blocks. |
1180
|
|
|
* |
1181
|
|
|
* @param array $stylePairs Array keyed by media type containing (arrays of) CSS strings |
1182
|
|
|
* @return array |
1183
|
|
|
*/ |
1184
|
|
|
public static function makeCombinedStyles( array $stylePairs ) { |
1185
|
|
|
$out = []; |
1186
|
|
|
foreach ( $stylePairs as $media => $styles ) { |
1187
|
|
|
// ResourceLoaderFileModule::getStyle can return the styles |
1188
|
|
|
// as a string or an array of strings. This is to allow separation in |
1189
|
|
|
// the front-end. |
1190
|
|
|
$styles = (array)$styles; |
1191
|
|
|
foreach ( $styles as $style ) { |
1192
|
|
|
$style = trim( $style ); |
1193
|
|
|
// Don't output an empty "@media print { }" block (bug 40498) |
1194
|
|
|
if ( $style !== '' ) { |
1195
|
|
|
// Transform the media type based on request params and config |
1196
|
|
|
// The way that this relies on $wgRequest to propagate request params is slightly evil |
1197
|
|
|
$media = OutputPage::transformCssMedia( $media ); |
1198
|
|
|
|
1199
|
|
|
if ( $media === '' || $media == 'all' ) { |
1200
|
|
|
$out[] = $style; |
1201
|
|
|
} elseif ( is_string( $media ) ) { |
1202
|
|
|
$out[] = "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "}"; |
1203
|
|
|
} |
1204
|
|
|
// else: skip |
1205
|
|
|
} |
1206
|
|
|
} |
1207
|
|
|
} |
1208
|
|
|
return $out; |
1209
|
|
|
} |
1210
|
|
|
|
1211
|
|
|
/** |
1212
|
|
|
* Returns a JS call to mw.loader.state, which sets the state of a |
1213
|
|
|
* module or modules to a given value. Has two calling conventions: |
1214
|
|
|
* |
1215
|
|
|
* - ResourceLoader::makeLoaderStateScript( $name, $state ): |
1216
|
|
|
* Set the state of a single module called $name to $state |
1217
|
|
|
* |
1218
|
|
|
* - ResourceLoader::makeLoaderStateScript( [ $name => $state, ... ] ): |
1219
|
|
|
* Set the state of modules with the given names to the given states |
1220
|
|
|
* |
1221
|
|
|
* @param string $name |
1222
|
|
|
* @param string $state |
1223
|
|
|
* @return string |
1224
|
|
|
*/ |
1225
|
|
View Code Duplication |
public static function makeLoaderStateScript( $name, $state = null ) { |
1226
|
|
|
if ( is_array( $name ) ) { |
1227
|
|
|
return Xml::encodeJsCall( |
1228
|
|
|
'mw.loader.state', |
1229
|
|
|
[ $name ], |
1230
|
|
|
ResourceLoader::inDebugMode() |
1231
|
|
|
); |
1232
|
|
|
} else { |
1233
|
|
|
return Xml::encodeJsCall( |
1234
|
|
|
'mw.loader.state', |
1235
|
|
|
[ $name, $state ], |
1236
|
|
|
ResourceLoader::inDebugMode() |
1237
|
|
|
); |
1238
|
|
|
} |
1239
|
|
|
} |
1240
|
|
|
|
1241
|
|
|
/** |
1242
|
|
|
* Returns JS code which calls the script given by $script. The script will |
1243
|
|
|
* be called with local variables name, version, dependencies and group, |
1244
|
|
|
* which will have values corresponding to $name, $version, $dependencies |
1245
|
|
|
* and $group as supplied. |
1246
|
|
|
* |
1247
|
|
|
* @param string $name Module name |
1248
|
|
|
* @param string $version Module version hash |
1249
|
|
|
* @param array $dependencies List of module names on which this module depends |
1250
|
|
|
* @param string $group Group which the module is in. |
1251
|
|
|
* @param string $source Source of the module, or 'local' if not foreign. |
1252
|
|
|
* @param string $script JavaScript code |
1253
|
|
|
* @return string |
1254
|
|
|
*/ |
1255
|
|
|
public static function makeCustomLoaderScript( $name, $version, $dependencies, |
1256
|
|
|
$group, $source, $script |
1257
|
|
|
) { |
1258
|
|
|
$script = str_replace( "\n", "\n\t", trim( $script ) ); |
1259
|
|
|
return Xml::encodeJsCall( |
1260
|
|
|
"( function ( name, version, dependencies, group, source ) {\n\t$script\n} )", |
1261
|
|
|
[ $name, $version, $dependencies, $group, $source ], |
1262
|
|
|
ResourceLoader::inDebugMode() |
1263
|
|
|
); |
1264
|
|
|
} |
1265
|
|
|
|
1266
|
|
|
private static function isEmptyObject( stdClass $obj ) { |
1267
|
|
|
foreach ( $obj as $key => $value ) { |
|
|
|
|
1268
|
|
|
return false; |
1269
|
|
|
} |
1270
|
|
|
return true; |
1271
|
|
|
} |
1272
|
|
|
|
1273
|
|
|
/** |
1274
|
|
|
* Remove empty values from the end of an array. |
1275
|
|
|
* |
1276
|
|
|
* Values considered empty: |
1277
|
|
|
* |
1278
|
|
|
* - null |
1279
|
|
|
* - [] |
1280
|
|
|
* - new XmlJsCode( '{}' ) |
1281
|
|
|
* - new stdClass() // (object) [] |
1282
|
|
|
* |
1283
|
|
|
* @param Array $array |
1284
|
|
|
*/ |
1285
|
|
|
private static function trimArray( array &$array ) { |
1286
|
|
|
$i = count( $array ); |
1287
|
|
|
while ( $i-- ) { |
1288
|
|
|
if ( $array[$i] === null |
1289
|
|
|
|| $array[$i] === [] |
1290
|
|
|
|| ( $array[$i] instanceof XmlJsCode && $array[$i]->value === '{}' ) |
1291
|
|
|
|| ( $array[$i] instanceof stdClass && self::isEmptyObject( $array[$i] ) ) |
1292
|
|
|
) { |
1293
|
|
|
unset( $array[$i] ); |
1294
|
|
|
} else { |
1295
|
|
|
break; |
1296
|
|
|
} |
1297
|
|
|
} |
1298
|
|
|
} |
1299
|
|
|
|
1300
|
|
|
/** |
1301
|
|
|
* Returns JS code which calls mw.loader.register with the given |
1302
|
|
|
* parameters. Has three calling conventions: |
1303
|
|
|
* |
1304
|
|
|
* - ResourceLoader::makeLoaderRegisterScript( $name, $version, |
1305
|
|
|
* $dependencies, $group, $source, $skip |
1306
|
|
|
* ): |
1307
|
|
|
* Register a single module. |
1308
|
|
|
* |
1309
|
|
|
* - ResourceLoader::makeLoaderRegisterScript( [ $name1, $name2 ] ): |
1310
|
|
|
* Register modules with the given names. |
1311
|
|
|
* |
1312
|
|
|
* - ResourceLoader::makeLoaderRegisterScript( [ |
1313
|
|
|
* [ $name1, $version1, $dependencies1, $group1, $source1, $skip1 ], |
1314
|
|
|
* [ $name2, $version2, $dependencies1, $group2, $source2, $skip2 ], |
1315
|
|
|
* ... |
1316
|
|
|
* ] ): |
1317
|
|
|
* Registers modules with the given names and parameters. |
1318
|
|
|
* |
1319
|
|
|
* @param string $name Module name |
1320
|
|
|
* @param string $version Module version hash |
1321
|
|
|
* @param array $dependencies List of module names on which this module depends |
1322
|
|
|
* @param string $group Group which the module is in |
1323
|
|
|
* @param string $source Source of the module, or 'local' if not foreign |
1324
|
|
|
* @param string $skip Script body of the skip function |
1325
|
|
|
* @return string |
1326
|
|
|
*/ |
1327
|
|
|
public static function makeLoaderRegisterScript( $name, $version = null, |
1328
|
|
|
$dependencies = null, $group = null, $source = null, $skip = null |
1329
|
|
|
) { |
1330
|
|
|
if ( is_array( $name ) ) { |
1331
|
|
|
// Build module name index |
1332
|
|
|
$index = []; |
1333
|
|
|
foreach ( $name as $i => &$module ) { |
1334
|
|
|
$index[$module[0]] = $i; |
1335
|
|
|
} |
1336
|
|
|
|
1337
|
|
|
// Transform dependency names into indexes when possible, they will be resolved by |
1338
|
|
|
// mw.loader.register on the other end |
1339
|
|
|
foreach ( $name as &$module ) { |
1340
|
|
|
if ( isset( $module[2] ) ) { |
1341
|
|
|
foreach ( $module[2] as &$dependency ) { |
1342
|
|
|
if ( isset( $index[$dependency] ) ) { |
1343
|
|
|
$dependency = $index[$dependency]; |
1344
|
|
|
} |
1345
|
|
|
} |
1346
|
|
|
} |
1347
|
|
|
} |
1348
|
|
|
|
1349
|
|
|
array_walk( $name, [ 'self', 'trimArray' ] ); |
1350
|
|
|
|
1351
|
|
|
return Xml::encodeJsCall( |
1352
|
|
|
'mw.loader.register', |
1353
|
|
|
[ $name ], |
1354
|
|
|
ResourceLoader::inDebugMode() |
1355
|
|
|
); |
1356
|
|
|
} else { |
1357
|
|
|
$registration = [ $name, $version, $dependencies, $group, $source, $skip ]; |
1358
|
|
|
self::trimArray( $registration ); |
1359
|
|
|
return Xml::encodeJsCall( |
1360
|
|
|
'mw.loader.register', |
1361
|
|
|
$registration, |
1362
|
|
|
ResourceLoader::inDebugMode() |
1363
|
|
|
); |
1364
|
|
|
} |
1365
|
|
|
} |
1366
|
|
|
|
1367
|
|
|
/** |
1368
|
|
|
* Returns JS code which calls mw.loader.addSource() with the given |
1369
|
|
|
* parameters. Has two calling conventions: |
1370
|
|
|
* |
1371
|
|
|
* - ResourceLoader::makeLoaderSourcesScript( $id, $properties ): |
1372
|
|
|
* Register a single source |
1373
|
|
|
* |
1374
|
|
|
* - ResourceLoader::makeLoaderSourcesScript( [ $id1 => $loadUrl, $id2 => $loadUrl, ... ] ); |
1375
|
|
|
* Register sources with the given IDs and properties. |
1376
|
|
|
* |
1377
|
|
|
* @param string $id Source ID |
1378
|
|
|
* @param string $loadUrl load.php url |
1379
|
|
|
* @return string |
1380
|
|
|
*/ |
1381
|
|
View Code Duplication |
public static function makeLoaderSourcesScript( $id, $loadUrl = null ) { |
1382
|
|
|
if ( is_array( $id ) ) { |
1383
|
|
|
return Xml::encodeJsCall( |
1384
|
|
|
'mw.loader.addSource', |
1385
|
|
|
[ $id ], |
1386
|
|
|
ResourceLoader::inDebugMode() |
1387
|
|
|
); |
1388
|
|
|
} else { |
1389
|
|
|
return Xml::encodeJsCall( |
1390
|
|
|
'mw.loader.addSource', |
1391
|
|
|
[ $id, $loadUrl ], |
1392
|
|
|
ResourceLoader::inDebugMode() |
1393
|
|
|
); |
1394
|
|
|
} |
1395
|
|
|
} |
1396
|
|
|
|
1397
|
|
|
/** |
1398
|
|
|
* Returns JS code which runs given JS code if the client-side framework is |
1399
|
|
|
* present. |
1400
|
|
|
* |
1401
|
|
|
* @deprecated since 1.25; use makeInlineScript instead |
1402
|
|
|
* @param string $script JavaScript code |
1403
|
|
|
* @return string |
1404
|
|
|
*/ |
1405
|
|
|
public static function makeLoaderConditionalScript( $script ) { |
1406
|
|
|
return '(window.RLQ=window.RLQ||[]).push(function(){' . |
1407
|
|
|
trim( $script ) . '});'; |
1408
|
|
|
} |
1409
|
|
|
|
1410
|
|
|
/** |
1411
|
|
|
* Construct an inline script tag with given JS code. |
1412
|
|
|
* |
1413
|
|
|
* The code will be wrapped in a closure, and it will be executed by ResourceLoader |
1414
|
|
|
* only if the client has adequate support for MediaWiki JavaScript code. |
1415
|
|
|
* |
1416
|
|
|
* @param string $script JavaScript code |
1417
|
|
|
* @return WrappedString HTML |
1418
|
|
|
*/ |
1419
|
|
|
public static function makeInlineScript( $script ) { |
1420
|
|
|
$js = self::makeLoaderConditionalScript( $script ); |
1421
|
|
|
return new WrappedString( |
1422
|
|
|
Html::inlineScript( $js ), |
1423
|
|
|
'<script>(window.RLQ=window.RLQ||[]).push(function(){', |
1424
|
|
|
'});</script>' |
1425
|
|
|
); |
1426
|
|
|
} |
1427
|
|
|
|
1428
|
|
|
/** |
1429
|
|
|
* Returns JS code which will set the MediaWiki configuration array to |
1430
|
|
|
* the given value. |
1431
|
|
|
* |
1432
|
|
|
* @param array $configuration List of configuration values keyed by variable name |
1433
|
|
|
* @return string |
1434
|
|
|
*/ |
1435
|
|
|
public static function makeConfigSetScript( array $configuration ) { |
1436
|
|
|
return Xml::encodeJsCall( |
1437
|
|
|
'mw.config.set', |
1438
|
|
|
[ $configuration ], |
1439
|
|
|
ResourceLoader::inDebugMode() |
1440
|
|
|
); |
1441
|
|
|
} |
1442
|
|
|
|
1443
|
|
|
/** |
1444
|
|
|
* Convert an array of module names to a packed query string. |
1445
|
|
|
* |
1446
|
|
|
* For example, [ 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ] |
1447
|
|
|
* becomes 'foo.bar,baz|bar.baz,quux' |
1448
|
|
|
* @param array $modules List of module names (strings) |
1449
|
|
|
* @return string Packed query string |
1450
|
|
|
*/ |
1451
|
|
|
public static function makePackedModulesString( $modules ) { |
1452
|
|
|
$groups = []; // [ prefix => [ suffixes ] ] |
1453
|
|
|
foreach ( $modules as $module ) { |
1454
|
|
|
$pos = strrpos( $module, '.' ); |
1455
|
|
|
$prefix = $pos === false ? '' : substr( $module, 0, $pos ); |
1456
|
|
|
$suffix = $pos === false ? $module : substr( $module, $pos + 1 ); |
1457
|
|
|
$groups[$prefix][] = $suffix; |
1458
|
|
|
} |
1459
|
|
|
|
1460
|
|
|
$arr = []; |
1461
|
|
|
foreach ( $groups as $prefix => $suffixes ) { |
1462
|
|
|
$p = $prefix === '' ? '' : $prefix . '.'; |
1463
|
|
|
$arr[] = $p . implode( ',', $suffixes ); |
1464
|
|
|
} |
1465
|
|
|
$str = implode( '|', $arr ); |
1466
|
|
|
return $str; |
1467
|
|
|
} |
1468
|
|
|
|
1469
|
|
|
/** |
1470
|
|
|
* Determine whether debug mode was requested |
1471
|
|
|
* Order of priority is 1) request param, 2) cookie, 3) $wg setting |
1472
|
|
|
* @return bool |
1473
|
|
|
*/ |
1474
|
|
|
public static function inDebugMode() { |
1475
|
|
|
if ( self::$debugMode === null ) { |
1476
|
|
|
global $wgRequest, $wgResourceLoaderDebug; |
1477
|
|
|
self::$debugMode = $wgRequest->getFuzzyBool( 'debug', |
1478
|
|
|
$wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) |
1479
|
|
|
); |
1480
|
|
|
} |
1481
|
|
|
return self::$debugMode; |
1482
|
|
|
} |
1483
|
|
|
|
1484
|
|
|
/** |
1485
|
|
|
* Reset static members used for caching. |
1486
|
|
|
* |
1487
|
|
|
* Global state and $wgRequest are evil, but we're using it right |
1488
|
|
|
* now and sometimes we need to be able to force ResourceLoader to |
1489
|
|
|
* re-evaluate the context because it has changed (e.g. in the test suite). |
1490
|
|
|
*/ |
1491
|
|
|
public static function clearCache() { |
1492
|
|
|
self::$debugMode = null; |
1493
|
|
|
} |
1494
|
|
|
|
1495
|
|
|
/** |
1496
|
|
|
* Build a load.php URL |
1497
|
|
|
* |
1498
|
|
|
* @since 1.24 |
1499
|
|
|
* @param string $source Name of the ResourceLoader source |
1500
|
|
|
* @param ResourceLoaderContext $context |
1501
|
|
|
* @param array $extraQuery |
1502
|
|
|
* @return string URL to load.php. May be protocol-relative if $wgLoadScript is, too. |
1503
|
|
|
*/ |
1504
|
|
|
public function createLoaderURL( $source, ResourceLoaderContext $context, |
1505
|
|
|
$extraQuery = [] |
1506
|
|
|
) { |
1507
|
|
|
$query = self::createLoaderQuery( $context, $extraQuery ); |
1508
|
|
|
$script = $this->getLoadScript( $source ); |
1509
|
|
|
|
1510
|
|
|
return wfAppendQuery( $script, $query ); |
1511
|
|
|
} |
1512
|
|
|
|
1513
|
|
|
/** |
1514
|
|
|
* Helper for createLoaderURL() |
1515
|
|
|
* |
1516
|
|
|
* @since 1.24 |
1517
|
|
|
* @see makeLoaderQuery |
1518
|
|
|
* @param ResourceLoaderContext $context |
1519
|
|
|
* @param array $extraQuery |
1520
|
|
|
* @return array |
1521
|
|
|
*/ |
1522
|
|
|
protected static function createLoaderQuery( ResourceLoaderContext $context, $extraQuery = [] ) { |
1523
|
|
|
return self::makeLoaderQuery( |
1524
|
|
|
$context->getModules(), |
1525
|
|
|
$context->getLanguage(), |
1526
|
|
|
$context->getSkin(), |
1527
|
|
|
$context->getUser(), |
1528
|
|
|
$context->getVersion(), |
1529
|
|
|
$context->getDebug(), |
1530
|
|
|
$context->getOnly(), |
1531
|
|
|
$context->getRequest()->getBool( 'printable' ), |
1532
|
|
|
$context->getRequest()->getBool( 'handheld' ), |
1533
|
|
|
$extraQuery |
1534
|
|
|
); |
1535
|
|
|
} |
1536
|
|
|
|
1537
|
|
|
/** |
1538
|
|
|
* Build a query array (array representation of query string) for load.php. Helper |
1539
|
|
|
* function for createLoaderURL(). |
1540
|
|
|
* |
1541
|
|
|
* @param array $modules |
1542
|
|
|
* @param string $lang |
1543
|
|
|
* @param string $skin |
1544
|
|
|
* @param string $user |
1545
|
|
|
* @param string $version |
1546
|
|
|
* @param bool $debug |
1547
|
|
|
* @param string $only |
1548
|
|
|
* @param bool $printable |
1549
|
|
|
* @param bool $handheld |
1550
|
|
|
* @param array $extraQuery |
1551
|
|
|
* |
1552
|
|
|
* @return array |
1553
|
|
|
*/ |
1554
|
|
|
public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, |
1555
|
|
|
$version = null, $debug = false, $only = null, $printable = false, |
1556
|
|
|
$handheld = false, $extraQuery = [] |
1557
|
|
|
) { |
1558
|
|
|
$query = [ |
1559
|
|
|
'modules' => self::makePackedModulesString( $modules ), |
1560
|
|
|
'lang' => $lang, |
1561
|
|
|
'skin' => $skin, |
1562
|
|
|
'debug' => $debug ? 'true' : 'false', |
1563
|
|
|
]; |
1564
|
|
|
if ( $user !== null ) { |
1565
|
|
|
$query['user'] = $user; |
1566
|
|
|
} |
1567
|
|
|
if ( $version !== null ) { |
1568
|
|
|
$query['version'] = $version; |
1569
|
|
|
} |
1570
|
|
|
if ( $only !== null ) { |
1571
|
|
|
$query['only'] = $only; |
1572
|
|
|
} |
1573
|
|
|
if ( $printable ) { |
1574
|
|
|
$query['printable'] = 1; |
1575
|
|
|
} |
1576
|
|
|
if ( $handheld ) { |
1577
|
|
|
$query['handheld'] = 1; |
1578
|
|
|
} |
1579
|
|
|
$query += $extraQuery; |
1580
|
|
|
|
1581
|
|
|
// Make queries uniform in order |
1582
|
|
|
ksort( $query ); |
1583
|
|
|
return $query; |
1584
|
|
|
} |
1585
|
|
|
|
1586
|
|
|
/** |
1587
|
|
|
* Check a module name for validity. |
1588
|
|
|
* |
1589
|
|
|
* Module names may not contain pipes (|), commas (,) or exclamation marks (!) and can be |
1590
|
|
|
* at most 255 bytes. |
1591
|
|
|
* |
1592
|
|
|
* @param string $moduleName Module name to check |
1593
|
|
|
* @return bool Whether $moduleName is a valid module name |
1594
|
|
|
*/ |
1595
|
|
|
public static function isValidModuleName( $moduleName ) { |
1596
|
|
|
return strcspn( $moduleName, '!,|', 0, 255 ) === strlen( $moduleName ); |
1597
|
|
|
} |
1598
|
|
|
|
1599
|
|
|
/** |
1600
|
|
|
* Returns LESS compiler set up for use with MediaWiki |
1601
|
|
|
* |
1602
|
|
|
* @since 1.27 |
1603
|
|
|
* @param array $extraVars Associative array of extra (i.e., other than the |
1604
|
|
|
* globally-configured ones) that should be used for compilation. |
1605
|
|
|
* @throws MWException |
1606
|
|
|
* @return Less_Parser |
1607
|
|
|
*/ |
1608
|
|
|
public function getLessCompiler( $extraVars = [] ) { |
1609
|
|
|
// When called from the installer, it is possible that a required PHP extension |
1610
|
|
|
// is missing (at least for now; see bug 47564). If this is the case, throw an |
1611
|
|
|
// exception (caught by the installer) to prevent a fatal error later on. |
1612
|
|
|
if ( !class_exists( 'Less_Parser' ) ) { |
1613
|
|
|
throw new MWException( 'MediaWiki requires the less.php parser' ); |
1614
|
|
|
} |
1615
|
|
|
|
1616
|
|
|
$parser = new Less_Parser; |
1617
|
|
|
$parser->ModifyVars( array_merge( $this->getLessVars(), $extraVars ) ); |
1618
|
|
|
$parser->SetImportDirs( |
1619
|
|
|
array_fill_keys( $this->config->get( 'ResourceLoaderLESSImportPaths' ), '' ) |
1620
|
|
|
); |
1621
|
|
|
$parser->SetOption( 'relativeUrls', false ); |
1622
|
|
|
|
1623
|
|
|
return $parser; |
1624
|
|
|
} |
1625
|
|
|
|
1626
|
|
|
/** |
1627
|
|
|
* Get global LESS variables. |
1628
|
|
|
* |
1629
|
|
|
* @since 1.27 |
1630
|
|
|
* @return array Map of variable names to string CSS values. |
1631
|
|
|
*/ |
1632
|
|
|
public function getLessVars() { |
1633
|
|
|
if ( !$this->lessVars ) { |
1634
|
|
|
$lessVars = $this->config->get( 'ResourceLoaderLESSVars' ); |
1635
|
|
|
Hooks::run( 'ResourceLoaderGetLessVars', [ &$lessVars ] ); |
1636
|
|
|
$this->lessVars = $lessVars; |
|
|
|
|
1637
|
|
|
} |
1638
|
|
|
return $this->lessVars; |
1639
|
|
|
} |
1640
|
|
|
} |
1641
|
|
|
|
This check looks for function or method calls that always return null and whose return value is assigned to a variable.
The method
getObject()
can return nothing but null, so it makes no sense to assign that value to a variable.The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.