Completed
Branch master (ee71c2)
by
unknown
26:37
created

ResourceLoaderModule::getName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
/**
3
 * Abstraction for ResourceLoader modules.
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
use Psr\Log\LoggerAwareInterface;
26
use Psr\Log\LoggerInterface;
27
use Psr\Log\NullLogger;
28
29
/**
30
 * Abstraction for ResourceLoader modules, with name registration and maxage functionality.
31
 */
32
abstract class ResourceLoaderModule implements LoggerAwareInterface {
33
	# Type of resource
34
	const TYPE_SCRIPTS = 'scripts';
35
	const TYPE_STYLES = 'styles';
36
	const TYPE_COMBINED = 'combined';
37
38
	# Desired load type
39
	// Module only has styles (loaded via <style> or <link rel=stylesheet>)
40
	const LOAD_STYLES = 'styles';
41
	// Module may have other resources (loaded via mw.loader from a script)
42
	const LOAD_GENERAL = 'general';
43
44
	# sitewide core module like a skin file or jQuery component
45
	const ORIGIN_CORE_SITEWIDE = 1;
46
47
	# per-user module generated by the software
48
	const ORIGIN_CORE_INDIVIDUAL = 2;
49
50
	# sitewide module generated from user-editable files, like MediaWiki:Common.js, or
51
	# modules accessible to multiple users, such as those generated by the Gadgets extension.
52
	const ORIGIN_USER_SITEWIDE = 3;
53
54
	# per-user module generated from user-editable files, like User:Me/vector.js
55
	const ORIGIN_USER_INDIVIDUAL = 4;
56
57
	# an access constant; make sure this is kept as the largest number in this group
58
	const ORIGIN_ALL = 10;
59
60
	# script and style modules form a hierarchy of trustworthiness, with core modules like
61
	# skins and jQuery as most trustworthy, and user scripts as least trustworthy.  We can
62
	# limit the types of scripts and styles we allow to load on, say, sensitive special
63
	# pages like Special:UserLogin and Special:Preferences
64
	protected $origin = self::ORIGIN_CORE_SITEWIDE;
65
66
	/* Protected Members */
67
68
	protected $name = null;
69
	protected $targets = [ 'desktop' ];
70
71
	// In-object cache for file dependencies
72
	protected $fileDeps = [];
73
	// In-object cache for message blob (keyed by language)
74
	protected $msgBlobs = [];
75
	// In-object cache for version hash
76
	protected $versionHash = [];
77
	// In-object cache for module content
78
	protected $contents = [];
79
80
	/**
81
	 * @var Config
82
	 */
83
	protected $config;
84
85
	/**
86
	 * @var LoggerInterface
87
	 */
88
	protected $logger;
89
90
	/* Methods */
91
92
	/**
93
	 * Get this module's name. This is set when the module is registered
94
	 * with ResourceLoader::register()
95
	 *
96
	 * @return string|null Name (string) or null if no name was set
97
	 */
98
	public function getName() {
99
		return $this->name;
100
	}
101
102
	/**
103
	 * Set this module's name. This is called by ResourceLoader::register()
104
	 * when registering the module. Other code should not call this.
105
	 *
106
	 * @param string $name Name
107
	 */
108
	public function setName( $name ) {
109
		$this->name = $name;
110
	}
111
112
	/**
113
	 * Get this module's origin. This is set when the module is registered
114
	 * with ResourceLoader::register()
115
	 *
116
	 * @return int ResourceLoaderModule class constant, the subclass default
117
	 *     if not set manually
118
	 */
119
	public function getOrigin() {
120
		return $this->origin;
121
	}
122
123
	/**
124
	 * @param ResourceLoaderContext $context
125
	 * @return bool
126
	 */
127
	public function getFlip( $context ) {
128
		global $wgContLang;
129
130
		return $wgContLang->getDir() !== $context->getDirection();
131
	}
132
133
	/**
134
	 * Get all JS for this module for a given language and skin.
135
	 * Includes all relevant JS except loader scripts.
136
	 *
137
	 * @param ResourceLoaderContext $context
138
	 * @return string JavaScript code
139
	 */
140
	public function getScript( ResourceLoaderContext $context ) {
141
		// Stub, override expected
142
		return '';
143
	}
144
145
	/**
146
	 * Takes named templates by the module and returns an array mapping.
147
	 *
148
	 * @return array of templates mapping template alias to content
149
	 */
150
	public function getTemplates() {
151
		// Stub, override expected.
152
		return [];
153
	}
154
155
	/**
156
	 * @return Config
157
	 * @since 1.24
158
	 */
159 View Code Duplication
	public function getConfig() {
160
		if ( $this->config === null ) {
161
			// Ugh, fall back to default
162
			$this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
0 ignored issues
show
Deprecated Code introduced by
The method ConfigFactory::getDefaultInstance() has been deprecated with message: since 1.27, use MediaWikiServices::getConfigFactory() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
163
		}
164
165
		return $this->config;
166
	}
167
168
	/**
169
	 * @param Config $config
170
	 * @since 1.24
171
	 */
172
	public function setConfig( Config $config ) {
173
		$this->config = $config;
174
	}
175
176
	/**
177
	 * @since 1.27
178
	 * @param LoggerInterface $logger
179
	 * @return null
180
	 */
181
	public function setLogger( LoggerInterface $logger ) {
182
		$this->logger = $logger;
183
	}
184
185
	/**
186
	 * @since 1.27
187
	 * @return LoggerInterface
188
	 */
189
	protected function getLogger() {
190
		if ( !$this->logger ) {
191
			$this->logger = new NullLogger();
192
		}
193
		return $this->logger;
194
	}
195
196
	/**
197
	 * Get the URL or URLs to load for this module's JS in debug mode.
198
	 * The default behavior is to return a load.php?only=scripts URL for
199
	 * the module, but file-based modules will want to override this to
200
	 * load the files directly.
201
	 *
202
	 * This function is called only when 1) we're in debug mode, 2) there
203
	 * is no only= parameter and 3) supportsURLLoading() returns true.
204
	 * #2 is important to prevent an infinite loop, therefore this function
205
	 * MUST return either an only= URL or a non-load.php URL.
206
	 *
207
	 * @param ResourceLoaderContext $context
208
	 * @return array Array of URLs
209
	 */
210 View Code Duplication
	public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
211
		$resourceLoader = $context->getResourceLoader();
212
		$derivative = new DerivativeResourceLoaderContext( $context );
213
		$derivative->setModules( [ $this->getName() ] );
214
		$derivative->setOnly( 'scripts' );
215
		$derivative->setDebug( true );
216
217
		$url = $resourceLoader->createLoaderURL(
218
			$this->getSource(),
219
			$derivative
220
		);
221
222
		return [ $url ];
223
	}
224
225
	/**
226
	 * Whether this module supports URL loading. If this function returns false,
227
	 * getScript() will be used even in cases (debug mode, no only param) where
228
	 * getScriptURLsForDebug() would normally be used instead.
229
	 * @return bool
230
	 */
231
	public function supportsURLLoading() {
232
		return true;
233
	}
234
235
	/**
236
	 * Get all CSS for this module for a given skin.
237
	 *
238
	 * @param ResourceLoaderContext $context
239
	 * @return array List of CSS strings or array of CSS strings keyed by media type.
240
	 *  like array( 'screen' => '.foo { width: 0 }' );
241
	 *  or array( 'screen' => array( '.foo { width: 0 }' ) );
242
	 */
243
	public function getStyles( ResourceLoaderContext $context ) {
244
		// Stub, override expected
245
		return [];
246
	}
247
248
	/**
249
	 * Get the URL or URLs to load for this module's CSS in debug mode.
250
	 * The default behavior is to return a load.php?only=styles URL for
251
	 * the module, but file-based modules will want to override this to
252
	 * load the files directly. See also getScriptURLsForDebug()
253
	 *
254
	 * @param ResourceLoaderContext $context
255
	 * @return array Array( mediaType => array( URL1, URL2, ... ), ... )
256
	 */
257 View Code Duplication
	public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
258
		$resourceLoader = $context->getResourceLoader();
259
		$derivative = new DerivativeResourceLoaderContext( $context );
260
		$derivative->setModules( [ $this->getName() ] );
261
		$derivative->setOnly( 'styles' );
262
		$derivative->setDebug( true );
263
264
		$url = $resourceLoader->createLoaderURL(
265
			$this->getSource(),
266
			$derivative
267
		);
268
269
		return [ 'all' => [ $url ] ];
270
	}
271
272
	/**
273
	 * Get the messages needed for this module.
274
	 *
275
	 * To get a JSON blob with messages, use MessageBlobStore::get()
276
	 *
277
	 * @return array List of message keys. Keys may occur more than once
278
	 */
279
	public function getMessages() {
280
		// Stub, override expected
281
		return [];
282
	}
283
284
	/**
285
	 * Get the group this module is in.
286
	 *
287
	 * @return string Group name
288
	 */
289
	public function getGroup() {
290
		// Stub, override expected
291
		return null;
292
	}
293
294
	/**
295
	 * Get the origin of this module. Should only be overridden for foreign modules.
296
	 *
297
	 * @return string Origin name, 'local' for local modules
298
	 */
299
	public function getSource() {
300
		// Stub, override expected
301
		return 'local';
302
	}
303
304
	/**
305
	 * Where on the HTML page should this module's JS be loaded?
306
	 *  - 'top': in the "<head>"
307
	 *  - 'bottom': at the bottom of the "<body>"
308
	 *
309
	 * @return string
310
	 */
311
	public function getPosition() {
312
		return 'bottom';
313
	}
314
315
	/**
316
	 * Whether this module's JS expects to work without the client-side ResourceLoader module.
317
	 * Returning true from this function will prevent mw.loader.state() call from being
318
	 * appended to the bottom of the script.
319
	 *
320
	 * @return bool
321
	 */
322
	public function isRaw() {
323
		return false;
324
	}
325
326
	/**
327
	 * Get a list of modules this module depends on.
328
	 *
329
	 * Dependency information is taken into account when loading a module
330
	 * on the client side.
331
	 *
332
	 * Note: It is expected that $context will be made non-optional in the near
333
	 * future.
334
	 *
335
	 * @param ResourceLoaderContext $context
336
	 * @return array List of module names as strings
337
	 */
338
	public function getDependencies( ResourceLoaderContext $context = null ) {
339
		// Stub, override expected
340
		return [];
341
	}
342
343
	/**
344
	 * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
345
	 *
346
	 * @return array Array of strings
347
	 */
348
	public function getTargets() {
349
		return $this->targets;
350
	}
351
352
	/**
353
	 * Get the module's load type.
354
	 *
355
	 * @since 1.28
356
	 * @return string ResourceLoaderModule LOAD_* constant
357
	 */
358
	public function getType() {
359
		return self::LOAD_GENERAL;
360
	}
361
362
	/**
363
	 * Get the skip function.
364
	 *
365
	 * Modules that provide fallback functionality can provide a "skip function". This
366
	 * function, if provided, will be passed along to the module registry on the client.
367
	 * When this module is loaded (either directly or as a dependency of another module),
368
	 * then this function is executed first. If the function returns true, the module will
369
	 * instantly be considered "ready" without requesting the associated module resources.
370
	 *
371
	 * The value returned here must be valid javascript for execution in a private function.
372
	 * It must not contain the "function () {" and "}" wrapper though.
373
	 *
374
	 * @return string|null A JavaScript function body returning a boolean value, or null
375
	 */
376
	public function getSkipFunction() {
377
		return null;
378
	}
379
380
	/**
381
	 * Get the files this module depends on indirectly for a given skin.
382
	 *
383
	 * These are only image files referenced by the module's stylesheet.
384
	 *
385
	 * @param ResourceLoaderContext $context
386
	 * @return array List of files
387
	 */
388
	protected function getFileDependencies( ResourceLoaderContext $context ) {
389
		$vary = $context->getSkin() . '|' . $context->getLanguage();
390
391
		// Try in-object cache first
392
		if ( !isset( $this->fileDeps[$vary] ) ) {
393
			$dbr = wfGetDB( DB_SLAVE );
394
			$deps = $dbr->selectField( 'module_deps',
395
				'md_deps',
396
				[
397
					'md_module' => $this->getName(),
398
					'md_skin' => $vary,
399
				],
400
				__METHOD__
401
			);
402
403
			if ( !is_null( $deps ) ) {
404
				$this->fileDeps[$vary] = self::expandRelativePaths(
405
					(array)FormatJson::decode( $deps, true )
406
				);
407
			} else {
408
				$this->fileDeps[$vary] = [];
409
			}
410
		}
411
		return $this->fileDeps[$vary];
412
	}
413
414
	/**
415
	 * Set in-object cache for file dependencies.
416
	 *
417
	 * This is used to retrieve data in batches. See ResourceLoader::preloadModuleInfo().
418
	 * To save the data, use saveFileDependencies().
419
	 *
420
	 * @param ResourceLoaderContext $context
421
	 * @param string[] $files Array of file names
422
	 */
423
	public function setFileDependencies( ResourceLoaderContext $context, $files ) {
424
		$vary = $context->getSkin() . '|' . $context->getLanguage();
425
		$this->fileDeps[$vary] = $files;
426
	}
427
428
	/**
429
	 * Set the files this module depends on indirectly for a given skin.
430
	 *
431
	 * @since 1.27
432
	 * @param ResourceLoaderContext $context
433
	 * @param array $localFileRefs List of files
434
	 */
435
	protected function saveFileDependencies( ResourceLoaderContext $context, $localFileRefs ) {
436
		// Normalise array
437
		$localFileRefs = array_values( array_unique( $localFileRefs ) );
438
		sort( $localFileRefs );
439
440
		try {
441
			// If the list has been modified since last time we cached it, update the cache
442
			if ( $localFileRefs !== $this->getFileDependencies( $context ) ) {
443
				$cache = ObjectCache::getLocalClusterInstance();
444
				$key = $cache->makeKey( __METHOD__, $this->getName() );
445
				$scopeLock = $cache->getScopedLock( $key, 0 );
446
				if ( !$scopeLock ) {
447
					return; // T124649; avoid write slams
448
				}
449
450
				$vary = $context->getSkin() . '|' . $context->getLanguage();
451
				$dbw = wfGetDB( DB_MASTER );
452
				$dbw->replace( 'module_deps',
453
					[ [ 'md_module', 'md_skin' ] ],
454
					[
455
						'md_module' => $this->getName(),
456
						'md_skin' => $vary,
457
						// Use relative paths to avoid ghost entries when $IP changes (T111481)
458
						'md_deps' => FormatJson::encode( self::getRelativePaths( $localFileRefs ) ),
459
					]
460
				);
461
462
				$dbw->onTransactionIdle( function () use ( &$scopeLock ) {
463
					ScopedCallback::consume( $scopeLock ); // release after commit
464
				} );
465
			}
466
		} catch ( Exception $e ) {
467
			wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" );
468
		}
469
	}
470
471
	/**
472
	 * Make file paths relative to MediaWiki directory.
473
	 *
474
	 * This is used to make file paths safe for storing in a database without the paths
475
	 * becoming stale or incorrect when MediaWiki is moved or upgraded (T111481).
476
	 *
477
	 * @since 1.27
478
	 * @param array $filePaths
479
	 * @return array
480
	 */
481
	public static function getRelativePaths( array $filePaths ) {
482
		global $IP;
483
		return array_map( function ( $path ) use ( $IP ) {
484
			return RelPath\getRelativePath( $path, $IP );
485
		}, $filePaths );
486
	}
487
488
	/**
489
	 * Expand directories relative to $IP.
490
	 *
491
	 * @since 1.27
492
	 * @param array $filePaths
493
	 * @return array
494
	 */
495
	public static function expandRelativePaths( array $filePaths ) {
496
		global $IP;
497
		return array_map( function ( $path ) use ( $IP ) {
498
			return RelPath\joinPath( $IP, $path );
499
		}, $filePaths );
500
	}
501
502
	/**
503
	 * Get the hash of the message blob.
504
	 *
505
	 * @since 1.27
506
	 * @param ResourceLoaderContext $context
507
	 * @return string|null JSON blob or null if module has no messages
508
	 */
509
	protected function getMessageBlob( ResourceLoaderContext $context ) {
510
		if ( !$this->getMessages() ) {
511
			// Don't bother consulting MessageBlobStore
512
			return null;
513
		}
514
		// Message blobs may only vary language, not by context keys
515
		$lang = $context->getLanguage();
516
		if ( !isset( $this->msgBlobs[$lang] ) ) {
517
			$this->getLogger()->warning( 'Message blob for {module} should have been preloaded', [
518
				'module' => $this->getName(),
519
			] );
520
			$store = $context->getResourceLoader()->getMessageBlobStore();
521
			$this->msgBlobs[$lang] = $store->getBlob( $this, $lang );
522
		}
523
		return $this->msgBlobs[$lang];
524
	}
525
526
	/**
527
	 * Set in-object cache for message blobs.
528
	 *
529
	 * Used to allow fetching of message blobs in batches. See ResourceLoader::preloadModuleInfo().
530
	 *
531
	 * @since 1.27
532
	 * @param string|null $blob JSON blob or null
533
	 * @param string $lang Language code
534
	 */
535
	public function setMessageBlob( $blob, $lang ) {
536
		$this->msgBlobs[$lang] = $blob;
537
	}
538
539
	/**
540
	 * Get module-specific LESS variables, if any.
541
	 *
542
	 * @since 1.27
543
	 * @param ResourceLoaderContext $context
544
	 * @return array Module-specific LESS variables.
545
	 */
546
	protected function getLessVars( ResourceLoaderContext $context ) {
547
		return [];
548
	}
549
550
	/**
551
	 * Get an array of this module's resources. Ready for serving to the web.
552
	 *
553
	 * @since 1.26
554
	 * @param ResourceLoaderContext $context
555
	 * @return array
556
	 */
557
	public function getModuleContent( ResourceLoaderContext $context ) {
558
		$contextHash = $context->getHash();
559
		// Cache this expensive operation. This calls builds the scripts, styles, and messages
560
		// content which typically involves filesystem and/or database access.
561
		if ( !array_key_exists( $contextHash, $this->contents ) ) {
562
			$this->contents[$contextHash] = $this->buildContent( $context );
563
		}
564
		return $this->contents[$contextHash];
565
	}
566
567
	/**
568
	 * Bundle all resources attached to this module into an array.
569
	 *
570
	 * @since 1.26
571
	 * @param ResourceLoaderContext $context
572
	 * @return array
573
	 */
574
	final protected function buildContent( ResourceLoaderContext $context ) {
575
		$rl = $context->getResourceLoader();
576
		$stats = RequestContext::getMain()->getStats();
0 ignored issues
show
Deprecated Code introduced by
The method RequestContext::getStats() has been deprecated with message: since 1.27 use a StatsdDataFactory from MediaWikiServices (preferably injected)

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
577
		$statStart = microtime( true );
578
579
		// Only include properties that are relevant to this context (e.g. only=scripts)
580
		// and that are non-empty (e.g. don't include "templates" for modules without
581
		// templates). This helps prevent invalidating cache for all modules when new
582
		// optional properties are introduced.
583
		$content = [];
584
585
		// Scripts
586
		if ( $context->shouldIncludeScripts() ) {
587
			// If we are in debug mode, we'll want to return an array of URLs if possible
588
			// However, we can't do this if the module doesn't support it
589
			// We also can't do this if there is an only= parameter, because we have to give
590
			// the module a way to return a load.php URL without causing an infinite loop
591
			if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $context->getOnly() of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
592
				$scripts = $this->getScriptURLsForDebug( $context );
593
			} else {
594
				$scripts = $this->getScript( $context );
595
				// rtrim() because there are usually a few line breaks
596
				// after the last ';'. A new line at EOF, a new line
597
				// added by ResourceLoaderFileModule::readScriptFiles, etc.
598
				if ( is_string( $scripts )
599
					&& strlen( $scripts )
600
					&& substr( rtrim( $scripts ), -1 ) !== ';'
601
				) {
602
					// Append semicolon to prevent weird bugs caused by files not
603
					// terminating their statements right (bug 27054)
604
					$scripts .= ";\n";
605
				}
606
			}
607
			$content['scripts'] = $scripts;
608
		}
609
610
		// Styles
611
		if ( $context->shouldIncludeStyles() ) {
612
			$styles = [];
613
			// Don't create empty stylesheets like array( '' => '' ) for modules
614
			// that don't *have* any stylesheets (bug 38024).
615
			$stylePairs = $this->getStyles( $context );
616
			if ( count( $stylePairs ) ) {
617
				// If we are in debug mode without &only= set, we'll want to return an array of URLs
618
				// See comment near shouldIncludeScripts() for more details
619
				if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $context->getOnly() of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
620
					$styles = [
621
						'url' => $this->getStyleURLsForDebug( $context )
622
					];
623
				} else {
624
					// Minify CSS before embedding in mw.loader.implement call
625
					// (unless in debug mode)
626
					if ( !$context->getDebug() ) {
627
						foreach ( $stylePairs as $media => $style ) {
628
							// Can be either a string or an array of strings.
629
							if ( is_array( $style ) ) {
630
								$stylePairs[$media] = [];
631
								foreach ( $style as $cssText ) {
632
									if ( is_string( $cssText ) ) {
633
										$stylePairs[$media][] =
634
											ResourceLoader::filter( 'minify-css', $cssText );
635
									}
636
								}
637
							} elseif ( is_string( $style ) ) {
638
								$stylePairs[$media] = ResourceLoader::filter( 'minify-css', $style );
639
							}
640
						}
641
					}
642
					// Wrap styles into @media groups as needed and flatten into a numerical array
643
					$styles = [
644
						'css' => $rl->makeCombinedStyles( $stylePairs )
645
					];
646
				}
647
			}
648
			$content['styles'] = $styles;
649
		}
650
651
		// Messages
652
		$blob = $this->getMessageBlob( $context );
653
		if ( $blob ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $blob of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
654
			$content['messagesBlob'] = $blob;
655
		}
656
657
		$templates = $this->getTemplates();
658
		if ( $templates ) {
659
			$content['templates'] = $templates;
660
		}
661
662
		$statTiming = microtime( true ) - $statStart;
663
		$statName = strtr( $this->getName(), '.', '_' );
664
		$stats->timing( "resourceloader_build.all", 1000 * $statTiming );
665
		$stats->timing( "resourceloader_build.$statName", 1000 * $statTiming );
666
667
		return $content;
668
	}
669
670
	/**
671
	 * Get a string identifying the current version of this module in a given context.
672
	 *
673
	 * Whenever anything happens that changes the module's response (e.g. scripts, styles, and
674
	 * messages) this value must change. This value is used to store module responses in cache.
675
	 * (Both client-side and server-side.)
676
	 *
677
	 * It is not recommended to override this directly. Use getDefinitionSummary() instead.
678
	 * If overridden, one must call the parent getVersionHash(), append data and re-hash.
679
	 *
680
	 * This method should be quick because it is frequently run by ResourceLoaderStartUpModule to
681
	 * propagate changes to the client and effectively invalidate cache.
682
	 *
683
	 * For backward-compatibility, the following optional data providers are automatically included:
684
	 *
685
	 * - getModifiedTime()
686
	 * - getModifiedHash()
687
	 *
688
	 * @since 1.26
689
	 * @param ResourceLoaderContext $context
690
	 * @return string Hash (should use ResourceLoader::makeHash)
691
	 */
692
	public function getVersionHash( ResourceLoaderContext $context ) {
693
		// The startup module produces a manifest with versions representing the entire module.
694
		// Typically, the request for the startup module itself has only=scripts. That must apply
695
		// only to the startup module content, and not to the module version computed here.
696
		$context = new DerivativeResourceLoaderContext( $context );
697
		$context->setModules( [] );
698
		// Version hash must cover all resources, regardless of startup request itself.
699
		$context->setOnly( null );
700
		// Compute version hash based on content, not debug urls.
701
		$context->setDebug( false );
702
703
		// Cache this somewhat expensive operation. Especially because some classes
704
		// (e.g. startup module) iterate more than once over all modules to get versions.
705
		$contextHash = $context->getHash();
706
		if ( !array_key_exists( $contextHash, $this->versionHash ) ) {
707
708
			if ( $this->enableModuleContentVersion() ) {
709
				// Detect changes directly
710
				$str = json_encode( $this->getModuleContent( $context ) );
711
			} else {
712
				// Infer changes based on definition and other metrics
713
				$summary = $this->getDefinitionSummary( $context );
714
				if ( !isset( $summary['_cacheEpoch'] ) ) {
715
					throw new LogicException( 'getDefinitionSummary must call parent method' );
716
				}
717
				$str = json_encode( $summary );
718
719
				$mtime = $this->getModifiedTime( $context );
0 ignored issues
show
Deprecated Code introduced by
The method ResourceLoaderModule::getModifiedTime() has been deprecated with message: since 1.26 Use getDefinitionSummary() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
720
				if ( $mtime !== null ) {
721
					// Support: MediaWiki 1.25 and earlier
722
					$str .= strval( $mtime );
723
				}
724
725
				$mhash = $this->getModifiedHash( $context );
0 ignored issues
show
Deprecated Code introduced by
The method ResourceLoaderModule::getModifiedHash() has been deprecated with message: since 1.26 Use getDefinitionSummary() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
726
				if ( $mhash !== null ) {
727
					// Support: MediaWiki 1.25 and earlier
728
					$str .= strval( $mhash );
729
				}
730
			}
731
732
			$this->versionHash[$contextHash] = ResourceLoader::makeHash( $str );
733
		}
734
		return $this->versionHash[$contextHash];
735
	}
736
737
	/**
738
	 * Whether to generate version hash based on module content.
739
	 *
740
	 * If a module requires database or file system access to build the module
741
	 * content, consider disabling this in favour of manually tracking relevant
742
	 * aspects in getDefinitionSummary(). See getVersionHash() for how this is used.
743
	 *
744
	 * @return bool
745
	 */
746
	public function enableModuleContentVersion() {
747
		return false;
748
	}
749
750
	/**
751
	 * Get the definition summary for this module.
752
	 *
753
	 * This is the method subclasses are recommended to use to track values in their
754
	 * version hash. Call this in getVersionHash() and pass it to e.g. json_encode.
755
	 *
756
	 * Subclasses must call the parent getDefinitionSummary() and build on that.
757
	 * It is recommended that each subclass appends its own new array. This prevents
758
	 * clashes or accidental overwrites of existing keys and gives each subclass
759
	 * its own scope for simple array keys.
760
	 *
761
	 * @code
762
	 *     $summary = parent::getDefinitionSummary( $context );
763
	 *     $summary[] = array(
764
	 *         'foo' => 123,
765
	 *         'bar' => 'quux',
766
	 *     );
767
	 *     return $summary;
768
	 * @endcode
769
	 *
770
	 * Return an array containing values from all significant properties of this
771
	 * module's definition.
772
	 *
773
	 * Be careful not to normalise too much. Especially preserve the order of things
774
	 * that carry significance in getScript and getStyles (T39812).
775
	 *
776
	 * Avoid including things that are insiginificant (e.g. order of message keys is
777
	 * insignificant and should be sorted to avoid unnecessary cache invalidation).
778
	 *
779
	 * This data structure must exclusively contain arrays and scalars as values (avoid
780
	 * object instances) to allow simple serialisation using json_encode.
781
	 *
782
	 * If modules have a hash or timestamp from another source, that may be incuded as-is.
783
	 *
784
	 * A number of utility methods are available to help you gather data. These are not
785
	 * called by default and must be included by the subclass' getDefinitionSummary().
786
	 *
787
	 * - getMessageBlob()
788
	 *
789
	 * @since 1.23
790
	 * @param ResourceLoaderContext $context
791
	 * @return array|null
792
	 */
793
	public function getDefinitionSummary( ResourceLoaderContext $context ) {
794
		return [
795
			'_class' => get_class( $this ),
796
			'_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ),
797
		];
798
	}
799
800
	/**
801
	 * Get this module's last modification timestamp for a given context.
802
	 *
803
	 * @deprecated since 1.26 Use getDefinitionSummary() instead
804
	 * @param ResourceLoaderContext $context Context object
805
	 * @return int|null UNIX timestamp
806
	 */
807
	public function getModifiedTime( ResourceLoaderContext $context ) {
808
		return null;
809
	}
810
811
	/**
812
	 * Helper method for providing a version hash to getVersionHash().
813
	 *
814
	 * @deprecated since 1.26 Use getDefinitionSummary() instead
815
	 * @param ResourceLoaderContext $context
816
	 * @return string|null Hash
817
	 */
818
	public function getModifiedHash( ResourceLoaderContext $context ) {
819
		return null;
820
	}
821
822
	/**
823
	 * Back-compat dummy for old subclass implementations of getModifiedTime().
824
	 *
825
	 * This method used to use ObjectCache to track when a hash was first seen. That principle
826
	 * stems from a time that ResourceLoader could only identify module versions by timestamp.
827
	 * That is no longer the case. Use getDefinitionSummary() directly.
828
	 *
829
	 * @deprecated since 1.26 Superseded by getVersionHash()
830
	 * @param ResourceLoaderContext $context
831
	 * @return int UNIX timestamp
832
	 */
833
	public function getHashMtime( ResourceLoaderContext $context ) {
834
		if ( !is_string( $this->getModifiedHash( $context ) ) ) {
0 ignored issues
show
Deprecated Code introduced by
The method ResourceLoaderModule::getModifiedHash() has been deprecated with message: since 1.26 Use getDefinitionSummary() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
835
			return 1;
836
		}
837
		// Dummy that is > 1
838
		return 2;
839
	}
840
841
	/**
842
	 * Back-compat dummy for old subclass implementations of getModifiedTime().
843
	 *
844
	 * @since 1.23
845
	 * @deprecated since 1.26 Superseded by getVersionHash()
846
	 * @param ResourceLoaderContext $context
847
	 * @return int UNIX timestamp
848
	 */
849
	public function getDefinitionMtime( ResourceLoaderContext $context ) {
850
		if ( $this->getDefinitionSummary( $context ) === null ) {
851
			return 1;
852
		}
853
		// Dummy that is > 1
854
		return 2;
855
	}
856
857
	/**
858
	 * Check whether this module is known to be empty. If a child class
859
	 * has an easy and cheap way to determine that this module is
860
	 * definitely going to be empty, it should override this method to
861
	 * return true in that case. Callers may optimize the request for this
862
	 * module away if this function returns true.
863
	 * @param ResourceLoaderContext $context
864
	 * @return bool
865
	 */
866
	public function isKnownEmpty( ResourceLoaderContext $context ) {
867
		return false;
868
	}
869
870
	/** @var JSParser Lazy-initialized; use self::javaScriptParser() */
871
	private static $jsParser;
872
	private static $parseCacheVersion = 1;
873
874
	/**
875
	 * Validate a given script file; if valid returns the original source.
876
	 * If invalid, returns replacement JS source that throws an exception.
877
	 *
878
	 * @param string $fileName
879
	 * @param string $contents
880
	 * @return string JS with the original, or a replacement error
881
	 */
882
	protected function validateScriptFile( $fileName, $contents ) {
883
		if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) {
884
			// Try for cache hit
885
			$cache = ObjectCache::getMainWANInstance();
886
			$key = $cache->makeKey(
887
				'resourceloader',
888
				'jsparse',
889
				self::$parseCacheVersion,
890
				md5( $contents )
891
			);
892
			$cacheEntry = $cache->get( $key );
893
			if ( is_string( $cacheEntry ) ) {
894
				return $cacheEntry;
895
			}
896
897
			$parser = self::javaScriptParser();
898
			try {
899
				$parser->parse( $contents, $fileName, 1 );
900
				$result = $contents;
901
			} catch ( Exception $e ) {
902
				// We'll save this to cache to avoid having to validate broken JS over and over...
903
				$err = $e->getMessage();
904
				$result = "mw.log.error(" .
905
					Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
906
			}
907
908
			$cache->set( $key, $result );
909
			return $result;
910
		} else {
911
			return $contents;
912
		}
913
	}
914
915
	/**
916
	 * @return JSParser
917
	 */
918
	protected static function javaScriptParser() {
919
		if ( !self::$jsParser ) {
920
			self::$jsParser = new JSParser();
921
		}
922
		return self::$jsParser;
923
	}
924
925
	/**
926
	 * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist.
927
	 * Defaults to 1.
928
	 *
929
	 * @param string $filePath File path
930
	 * @return int UNIX timestamp
931
	 */
932
	protected static function safeFilemtime( $filePath ) {
933
		MediaWiki\suppressWarnings();
934
		$mtime = filemtime( $filePath ) ?: 1;
935
		MediaWiki\restoreWarnings();
936
		return $mtime;
937
	}
938
939
	/**
940
	 * Compute a non-cryptographic string hash of a file's contents.
941
	 * If the file does not exist or cannot be read, returns an empty string.
942
	 *
943
	 * @since 1.26 Uses MD4 instead of SHA1.
944
	 * @param string $filePath File path
945
	 * @return string Hash
946
	 */
947
	protected static function safeFileHash( $filePath ) {
948
		return FileContentsHasher::getFileContentsHash( $filePath );
949
	}
950
}
951