Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/resourceloader/ResourceLoaderModule.php (4 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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
use Wikimedia\ScopedCallback;
0 ignored issues
show
This use statement conflicts with another class in this namespace, ScopedCallback.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
29
30
/**
31
 * Abstraction for ResourceLoader modules, with name registration and maxage functionality.
32
 */
33
abstract class ResourceLoaderModule implements LoggerAwareInterface {
34
	# Type of resource
35
	const TYPE_SCRIPTS = 'scripts';
36
	const TYPE_STYLES = 'styles';
37
	const TYPE_COMBINED = 'combined';
38
39
	# Desired load type
40
	// Module only has styles (loaded via <style> or <link rel=stylesheet>)
41
	const LOAD_STYLES = 'styles';
42
	// Module may have other resources (loaded via mw.loader from a script)
43
	const LOAD_GENERAL = 'general';
44
45
	# sitewide core module like a skin file or jQuery component
46
	const ORIGIN_CORE_SITEWIDE = 1;
47
48
	# per-user module generated by the software
49
	const ORIGIN_CORE_INDIVIDUAL = 2;
50
51
	# sitewide module generated from user-editable files, like MediaWiki:Common.js, or
52
	# modules accessible to multiple users, such as those generated by the Gadgets extension.
53
	const ORIGIN_USER_SITEWIDE = 3;
54
55
	# per-user module generated from user-editable files, like User:Me/vector.js
56
	const ORIGIN_USER_INDIVIDUAL = 4;
57
58
	# an access constant; make sure this is kept as the largest number in this group
59
	const ORIGIN_ALL = 10;
60
61
	# script and style modules form a hierarchy of trustworthiness, with core modules like
62
	# skins and jQuery as most trustworthy, and user scripts as least trustworthy.  We can
63
	# limit the types of scripts and styles we allow to load on, say, sensitive special
64
	# pages like Special:UserLogin and Special:Preferences
65
	protected $origin = self::ORIGIN_CORE_SITEWIDE;
66
67
	/* Protected Members */
68
69
	protected $name = null;
70
	protected $targets = [ 'desktop' ];
71
72
	// In-object cache for file dependencies
73
	protected $fileDeps = [];
74
	// In-object cache for message blob (keyed by language)
75
	protected $msgBlobs = [];
76
	// In-object cache for version hash
77
	protected $versionHash = [];
78
	// In-object cache for module content
79
	protected $contents = [];
80
81
	/**
82
	 * @var Config
83
	 */
84
	protected $config;
85
86
	/**
87
	 * @var array|bool
88
	 */
89
	protected $deprecated = false;
90
91
	/**
92
	 * @var LoggerInterface
93
	 */
94
	protected $logger;
95
96
	/* Methods */
97
98
	/**
99
	 * Get this module's name. This is set when the module is registered
100
	 * with ResourceLoader::register()
101
	 *
102
	 * @return string|null Name (string) or null if no name was set
103
	 */
104
	public function getName() {
105
		return $this->name;
106
	}
107
108
	/**
109
	 * Set this module's name. This is called by ResourceLoader::register()
110
	 * when registering the module. Other code should not call this.
111
	 *
112
	 * @param string $name Name
113
	 */
114
	public function setName( $name ) {
115
		$this->name = $name;
116
	}
117
118
	/**
119
	 * Get this module's origin. This is set when the module is registered
120
	 * with ResourceLoader::register()
121
	 *
122
	 * @return int ResourceLoaderModule class constant, the subclass default
123
	 *     if not set manually
124
	 */
125
	public function getOrigin() {
126
		return $this->origin;
127
	}
128
129
	/**
130
	 * @param ResourceLoaderContext $context
131
	 * @return bool
132
	 */
133
	public function getFlip( $context ) {
134
		global $wgContLang;
135
136
		return $wgContLang->getDir() !== $context->getDirection();
137
	}
138
139
	/**
140
	 * Get JS representing deprecation information for the current module if available
141
	 *
142
	 * @return string JavaScript code
143
	 */
144
	protected function getDeprecationInformation() {
145
		$deprecationInfo = $this->deprecated;
146
		if ( $deprecationInfo ) {
147
			$name = $this->getName();
148
			$warning = 'This page is using the deprecated ResourceLoader module "' . $name . '".';
149
			if ( !is_bool( $deprecationInfo ) && isset( $deprecationInfo['message'] ) ) {
150
				$warning .= "\n" . $deprecationInfo['message'];
151
			}
152
			return Xml::encodeJsCall(
153
				'mw.log.warn',
154
				[ $warning ]
155
			);
156
		} else {
157
			return '';
158
		}
159
	}
160
161
	/**
162
	 * Get all JS for this module for a given language and skin.
163
	 * Includes all relevant JS except loader scripts.
164
	 *
165
	 * @param ResourceLoaderContext $context
166
	 * @return string JavaScript code
167
	 */
168
	public function getScript( ResourceLoaderContext $context ) {
169
		// Stub, override expected
170
		return '';
171
	}
172
173
	/**
174
	 * Takes named templates by the module and returns an array mapping.
175
	 *
176
	 * @return array of templates mapping template alias to content
177
	 */
178
	public function getTemplates() {
179
		// Stub, override expected.
180
		return [];
181
	}
182
183
	/**
184
	 * @return Config
185
	 * @since 1.24
186
	 */
187 View Code Duplication
	public function getConfig() {
188
		if ( $this->config === null ) {
189
			// Ugh, fall back to default
190
			$this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
191
		}
192
193
		return $this->config;
194
	}
195
196
	/**
197
	 * @param Config $config
198
	 * @since 1.24
199
	 */
200
	public function setConfig( Config $config ) {
201
		$this->config = $config;
202
	}
203
204
	/**
205
	 * @since 1.27
206
	 * @param LoggerInterface $logger
207
	 * @return null
208
	 */
209
	public function setLogger( LoggerInterface $logger ) {
210
		$this->logger = $logger;
211
	}
212
213
	/**
214
	 * @since 1.27
215
	 * @return LoggerInterface
216
	 */
217
	protected function getLogger() {
218
		if ( !$this->logger ) {
219
			$this->logger = new NullLogger();
220
		}
221
		return $this->logger;
222
	}
223
224
	/**
225
	 * Get the URL or URLs to load for this module's JS in debug mode.
226
	 * The default behavior is to return a load.php?only=scripts URL for
227
	 * the module, but file-based modules will want to override this to
228
	 * load the files directly.
229
	 *
230
	 * This function is called only when 1) we're in debug mode, 2) there
231
	 * is no only= parameter and 3) supportsURLLoading() returns true.
232
	 * #2 is important to prevent an infinite loop, therefore this function
233
	 * MUST return either an only= URL or a non-load.php URL.
234
	 *
235
	 * @param ResourceLoaderContext $context
236
	 * @return array Array of URLs
237
	 */
238 View Code Duplication
	public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
239
		$resourceLoader = $context->getResourceLoader();
240
		$derivative = new DerivativeResourceLoaderContext( $context );
241
		$derivative->setModules( [ $this->getName() ] );
242
		$derivative->setOnly( 'scripts' );
243
		$derivative->setDebug( true );
244
245
		$url = $resourceLoader->createLoaderURL(
246
			$this->getSource(),
247
			$derivative
248
		);
249
250
		return [ $url ];
251
	}
252
253
	/**
254
	 * Whether this module supports URL loading. If this function returns false,
255
	 * getScript() will be used even in cases (debug mode, no only param) where
256
	 * getScriptURLsForDebug() would normally be used instead.
257
	 * @return bool
258
	 */
259
	public function supportsURLLoading() {
260
		return true;
261
	}
262
263
	/**
264
	 * Get all CSS for this module for a given skin.
265
	 *
266
	 * @param ResourceLoaderContext $context
267
	 * @return array List of CSS strings or array of CSS strings keyed by media type.
268
	 *  like [ 'screen' => '.foo { width: 0 }' ];
269
	 *  or [ 'screen' => [ '.foo { width: 0 }' ] ];
270
	 */
271
	public function getStyles( ResourceLoaderContext $context ) {
272
		// Stub, override expected
273
		return [];
274
	}
275
276
	/**
277
	 * Get the URL or URLs to load for this module's CSS in debug mode.
278
	 * The default behavior is to return a load.php?only=styles URL for
279
	 * the module, but file-based modules will want to override this to
280
	 * load the files directly. See also getScriptURLsForDebug()
281
	 *
282
	 * @param ResourceLoaderContext $context
283
	 * @return array [ mediaType => [ URL1, URL2, ... ], ... ]
284
	 */
285 View Code Duplication
	public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
286
		$resourceLoader = $context->getResourceLoader();
287
		$derivative = new DerivativeResourceLoaderContext( $context );
288
		$derivative->setModules( [ $this->getName() ] );
289
		$derivative->setOnly( 'styles' );
290
		$derivative->setDebug( true );
291
292
		$url = $resourceLoader->createLoaderURL(
293
			$this->getSource(),
294
			$derivative
295
		);
296
297
		return [ 'all' => [ $url ] ];
298
	}
299
300
	/**
301
	 * Get the messages needed for this module.
302
	 *
303
	 * To get a JSON blob with messages, use MessageBlobStore::get()
304
	 *
305
	 * @return array List of message keys. Keys may occur more than once
306
	 */
307
	public function getMessages() {
308
		// Stub, override expected
309
		return [];
310
	}
311
312
	/**
313
	 * Get the group this module is in.
314
	 *
315
	 * @return string Group name
316
	 */
317
	public function getGroup() {
318
		// Stub, override expected
319
		return null;
320
	}
321
322
	/**
323
	 * Get the origin of this module. Should only be overridden for foreign modules.
324
	 *
325
	 * @return string Origin name, 'local' for local modules
326
	 */
327
	public function getSource() {
328
		// Stub, override expected
329
		return 'local';
330
	}
331
332
	/**
333
	 * Where on the HTML page should this module's JS be loaded?
334
	 *  - 'top': in the "<head>"
335
	 *  - 'bottom': at the bottom of the "<body>"
336
	 *
337
	 * @return string
338
	 */
339
	public function getPosition() {
340
		return 'bottom';
341
	}
342
343
	/**
344
	 * Whether this module's JS expects to work without the client-side ResourceLoader module.
345
	 * Returning true from this function will prevent mw.loader.state() call from being
346
	 * appended to the bottom of the script.
347
	 *
348
	 * @return bool
349
	 */
350
	public function isRaw() {
351
		return false;
352
	}
353
354
	/**
355
	 * Get a list of modules this module depends on.
356
	 *
357
	 * Dependency information is taken into account when loading a module
358
	 * on the client side.
359
	 *
360
	 * Note: It is expected that $context will be made non-optional in the near
361
	 * future.
362
	 *
363
	 * @param ResourceLoaderContext $context
364
	 * @return array List of module names as strings
365
	 */
366
	public function getDependencies( ResourceLoaderContext $context = null ) {
367
		// Stub, override expected
368
		return [];
369
	}
370
371
	/**
372
	 * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
373
	 *
374
	 * @return array Array of strings
375
	 */
376
	public function getTargets() {
377
		return $this->targets;
378
	}
379
380
	/**
381
	 * Get the module's load type.
382
	 *
383
	 * @since 1.28
384
	 * @return string ResourceLoaderModule LOAD_* constant
385
	 */
386
	public function getType() {
387
		return self::LOAD_GENERAL;
388
	}
389
390
	/**
391
	 * Get the skip function.
392
	 *
393
	 * Modules that provide fallback functionality can provide a "skip function". This
394
	 * function, if provided, will be passed along to the module registry on the client.
395
	 * When this module is loaded (either directly or as a dependency of another module),
396
	 * then this function is executed first. If the function returns true, the module will
397
	 * instantly be considered "ready" without requesting the associated module resources.
398
	 *
399
	 * The value returned here must be valid javascript for execution in a private function.
400
	 * It must not contain the "function () {" and "}" wrapper though.
401
	 *
402
	 * @return string|null A JavaScript function body returning a boolean value, or null
403
	 */
404
	public function getSkipFunction() {
405
		return null;
406
	}
407
408
	/**
409
	 * Get the files this module depends on indirectly for a given skin.
410
	 *
411
	 * These are only image files referenced by the module's stylesheet.
412
	 *
413
	 * @param ResourceLoaderContext $context
414
	 * @return array List of files
415
	 */
416
	protected function getFileDependencies( ResourceLoaderContext $context ) {
417
		$vary = $context->getSkin() . '|' . $context->getLanguage();
418
419
		// Try in-object cache first
420
		if ( !isset( $this->fileDeps[$vary] ) ) {
421
			$dbr = wfGetDB( DB_REPLICA );
422
			$deps = $dbr->selectField( 'module_deps',
423
				'md_deps',
424
				[
425
					'md_module' => $this->getName(),
426
					'md_skin' => $vary,
427
				],
428
				__METHOD__
429
			);
430
431
			if ( !is_null( $deps ) ) {
432
				$this->fileDeps[$vary] = self::expandRelativePaths(
433
					(array)FormatJson::decode( $deps, true )
434
				);
435
			} else {
436
				$this->fileDeps[$vary] = [];
437
			}
438
		}
439
		return $this->fileDeps[$vary];
440
	}
441
442
	/**
443
	 * Set in-object cache for file dependencies.
444
	 *
445
	 * This is used to retrieve data in batches. See ResourceLoader::preloadModuleInfo().
446
	 * To save the data, use saveFileDependencies().
447
	 *
448
	 * @param ResourceLoaderContext $context
449
	 * @param string[] $files Array of file names
450
	 */
451
	public function setFileDependencies( ResourceLoaderContext $context, $files ) {
452
		$vary = $context->getSkin() . '|' . $context->getLanguage();
453
		$this->fileDeps[$vary] = $files;
454
	}
455
456
	/**
457
	 * Set the files this module depends on indirectly for a given skin.
458
	 *
459
	 * @since 1.27
460
	 * @param ResourceLoaderContext $context
461
	 * @param array $localFileRefs List of files
462
	 */
463
	protected function saveFileDependencies( ResourceLoaderContext $context, $localFileRefs ) {
464
		// Normalise array
465
		$localFileRefs = array_values( array_unique( $localFileRefs ) );
466
		sort( $localFileRefs );
467
468
		try {
469
			// If the list has been modified since last time we cached it, update the cache
470
			if ( $localFileRefs !== $this->getFileDependencies( $context ) ) {
471
				$cache = ObjectCache::getLocalClusterInstance();
472
				$key = $cache->makeKey( __METHOD__, $this->getName() );
473
				$scopeLock = $cache->getScopedLock( $key, 0 );
474
				if ( !$scopeLock ) {
475
					return; // T124649; avoid write slams
476
				}
477
478
				$vary = $context->getSkin() . '|' . $context->getLanguage();
479
				$dbw = wfGetDB( DB_MASTER );
480
				$dbw->replace( 'module_deps',
481
					[ [ 'md_module', 'md_skin' ] ],
482
					[
483
						'md_module' => $this->getName(),
484
						'md_skin' => $vary,
485
						// Use relative paths to avoid ghost entries when $IP changes (T111481)
486
						'md_deps' => FormatJson::encode( self::getRelativePaths( $localFileRefs ) ),
487
					]
488
				);
489
490
				if ( $dbw->trxLevel() ) {
491
					$dbw->onTransactionResolution(
492
						function () use ( &$scopeLock ) {
493
							ScopedCallback::consume( $scopeLock ); // release after commit
494
						},
495
						__METHOD__
496
					);
497
				}
498
			}
499
		} catch ( Exception $e ) {
500
			wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" );
501
		}
502
	}
503
504
	/**
505
	 * Make file paths relative to MediaWiki directory.
506
	 *
507
	 * This is used to make file paths safe for storing in a database without the paths
508
	 * becoming stale or incorrect when MediaWiki is moved or upgraded (T111481).
509
	 *
510
	 * @since 1.27
511
	 * @param array $filePaths
512
	 * @return array
513
	 */
514
	public static function getRelativePaths( array $filePaths ) {
515
		global $IP;
516
		return array_map( function ( $path ) use ( $IP ) {
517
			return RelPath\getRelativePath( $path, $IP );
518
		}, $filePaths );
519
	}
520
521
	/**
522
	 * Expand directories relative to $IP.
523
	 *
524
	 * @since 1.27
525
	 * @param array $filePaths
526
	 * @return array
527
	 */
528
	public static function expandRelativePaths( array $filePaths ) {
529
		global $IP;
530
		return array_map( function ( $path ) use ( $IP ) {
531
			return RelPath\joinPath( $IP, $path );
532
		}, $filePaths );
533
	}
534
535
	/**
536
	 * Get the hash of the message blob.
537
	 *
538
	 * @since 1.27
539
	 * @param ResourceLoaderContext $context
540
	 * @return string|null JSON blob or null if module has no messages
541
	 */
542
	protected function getMessageBlob( ResourceLoaderContext $context ) {
543
		if ( !$this->getMessages() ) {
544
			// Don't bother consulting MessageBlobStore
545
			return null;
546
		}
547
		// Message blobs may only vary language, not by context keys
548
		$lang = $context->getLanguage();
549
		if ( !isset( $this->msgBlobs[$lang] ) ) {
550
			$this->getLogger()->warning( 'Message blob for {module} should have been preloaded', [
551
				'module' => $this->getName(),
552
			] );
553
			$store = $context->getResourceLoader()->getMessageBlobStore();
554
			$this->msgBlobs[$lang] = $store->getBlob( $this, $lang );
555
		}
556
		return $this->msgBlobs[$lang];
557
	}
558
559
	/**
560
	 * Set in-object cache for message blobs.
561
	 *
562
	 * Used to allow fetching of message blobs in batches. See ResourceLoader::preloadModuleInfo().
563
	 *
564
	 * @since 1.27
565
	 * @param string|null $blob JSON blob or null
566
	 * @param string $lang Language code
567
	 */
568
	public function setMessageBlob( $blob, $lang ) {
569
		$this->msgBlobs[$lang] = $blob;
570
	}
571
572
	/**
573
	 * Get module-specific LESS variables, if any.
574
	 *
575
	 * @since 1.27
576
	 * @param ResourceLoaderContext $context
577
	 * @return array Module-specific LESS variables.
578
	 */
579
	protected function getLessVars( ResourceLoaderContext $context ) {
580
		return [];
581
	}
582
583
	/**
584
	 * Get an array of this module's resources. Ready for serving to the web.
585
	 *
586
	 * @since 1.26
587
	 * @param ResourceLoaderContext $context
588
	 * @return array
589
	 */
590
	public function getModuleContent( ResourceLoaderContext $context ) {
591
		$contextHash = $context->getHash();
592
		// Cache this expensive operation. This calls builds the scripts, styles, and messages
593
		// content which typically involves filesystem and/or database access.
594
		if ( !array_key_exists( $contextHash, $this->contents ) ) {
595
			$this->contents[$contextHash] = $this->buildContent( $context );
596
		}
597
		return $this->contents[$contextHash];
598
	}
599
600
	/**
601
	 * Bundle all resources attached to this module into an array.
602
	 *
603
	 * @since 1.26
604
	 * @param ResourceLoaderContext $context
605
	 * @return array
606
	 */
607
	final protected function buildContent( ResourceLoaderContext $context ) {
608
		$rl = $context->getResourceLoader();
609
		$stats = RequestContext::getMain()->getStats();
610
		$statStart = microtime( true );
611
612
		// Only include properties that are relevant to this context (e.g. only=scripts)
613
		// and that are non-empty (e.g. don't include "templates" for modules without
614
		// templates). This helps prevent invalidating cache for all modules when new
615
		// optional properties are introduced.
616
		$content = [];
617
618
		// Scripts
619
		if ( $context->shouldIncludeScripts() ) {
620
			// If we are in debug mode, we'll want to return an array of URLs if possible
621
			// However, we can't do this if the module doesn't support it
622
			// We also can't do this if there is an only= parameter, because we have to give
623
			// the module a way to return a load.php URL without causing an infinite loop
624
			if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $context->getOnly() of type string|null 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...
625
				$scripts = $this->getScriptURLsForDebug( $context );
626
			} else {
627
				$scripts = $this->getScript( $context );
628
				// rtrim() because there are usually a few line breaks
629
				// after the last ';'. A new line at EOF, a new line
630
				// added by ResourceLoaderFileModule::readScriptFiles, etc.
631
				if ( is_string( $scripts )
632
					&& strlen( $scripts )
633
					&& substr( rtrim( $scripts ), -1 ) !== ';'
634
				) {
635
					// Append semicolon to prevent weird bugs caused by files not
636
					// terminating their statements right (bug 27054)
637
					$scripts .= ";\n";
638
				}
639
			}
640
			$content['scripts'] = $scripts;
641
		}
642
643
		// Styles
644
		if ( $context->shouldIncludeStyles() ) {
645
			$styles = [];
646
			// Don't create empty stylesheets like [ '' => '' ] for modules
647
			// that don't *have* any stylesheets (bug 38024).
648
			$stylePairs = $this->getStyles( $context );
649
			if ( count( $stylePairs ) ) {
650
				// If we are in debug mode without &only= set, we'll want to return an array of URLs
651
				// See comment near shouldIncludeScripts() for more details
652
				if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $context->getOnly() of type string|null 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...
653
					$styles = [
654
						'url' => $this->getStyleURLsForDebug( $context )
655
					];
656
				} else {
657
					// Minify CSS before embedding in mw.loader.implement call
658
					// (unless in debug mode)
659
					if ( !$context->getDebug() ) {
660
						foreach ( $stylePairs as $media => $style ) {
661
							// Can be either a string or an array of strings.
662
							if ( is_array( $style ) ) {
663
								$stylePairs[$media] = [];
664
								foreach ( $style as $cssText ) {
665
									if ( is_string( $cssText ) ) {
666
										$stylePairs[$media][] =
667
											ResourceLoader::filter( 'minify-css', $cssText );
668
									}
669
								}
670
							} elseif ( is_string( $style ) ) {
671
								$stylePairs[$media] = ResourceLoader::filter( 'minify-css', $style );
672
							}
673
						}
674
					}
675
					// Wrap styles into @media groups as needed and flatten into a numerical array
676
					$styles = [
677
						'css' => $rl->makeCombinedStyles( $stylePairs )
678
					];
679
				}
680
			}
681
			$content['styles'] = $styles;
682
		}
683
684
		// Messages
685
		$blob = $this->getMessageBlob( $context );
686
		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...
687
			$content['messagesBlob'] = $blob;
688
		}
689
690
		$templates = $this->getTemplates();
691
		if ( $templates ) {
692
			$content['templates'] = $templates;
693
		}
694
695
		$statTiming = microtime( true ) - $statStart;
696
		$statName = strtr( $this->getName(), '.', '_' );
697
		$stats->timing( "resourceloader_build.all", 1000 * $statTiming );
698
		$stats->timing( "resourceloader_build.$statName", 1000 * $statTiming );
699
700
		return $content;
701
	}
702
703
	/**
704
	 * Get a string identifying the current version of this module in a given context.
705
	 *
706
	 * Whenever anything happens that changes the module's response (e.g. scripts, styles, and
707
	 * messages) this value must change. This value is used to store module responses in cache.
708
	 * (Both client-side and server-side.)
709
	 *
710
	 * It is not recommended to override this directly. Use getDefinitionSummary() instead.
711
	 * If overridden, one must call the parent getVersionHash(), append data and re-hash.
712
	 *
713
	 * This method should be quick because it is frequently run by ResourceLoaderStartUpModule to
714
	 * propagate changes to the client and effectively invalidate cache.
715
	 *
716
	 * For backward-compatibility, the following optional data providers are automatically included:
717
	 *
718
	 * - getModifiedTime()
719
	 * - getModifiedHash()
720
	 *
721
	 * @since 1.26
722
	 * @param ResourceLoaderContext $context
723
	 * @return string Hash (should use ResourceLoader::makeHash)
724
	 */
725
	public function getVersionHash( ResourceLoaderContext $context ) {
726
		// The startup module produces a manifest with versions representing the entire module.
727
		// Typically, the request for the startup module itself has only=scripts. That must apply
728
		// only to the startup module content, and not to the module version computed here.
729
		$context = new DerivativeResourceLoaderContext( $context );
730
		$context->setModules( [] );
731
		// Version hash must cover all resources, regardless of startup request itself.
732
		$context->setOnly( null );
733
		// Compute version hash based on content, not debug urls.
734
		$context->setDebug( false );
735
736
		// Cache this somewhat expensive operation. Especially because some classes
737
		// (e.g. startup module) iterate more than once over all modules to get versions.
738
		$contextHash = $context->getHash();
739
		if ( !array_key_exists( $contextHash, $this->versionHash ) ) {
740
741
			if ( $this->enableModuleContentVersion() ) {
742
				// Detect changes directly
743
				$str = json_encode( $this->getModuleContent( $context ) );
744
			} else {
745
				// Infer changes based on definition and other metrics
746
				$summary = $this->getDefinitionSummary( $context );
747
				if ( !isset( $summary['_cacheEpoch'] ) ) {
748
					throw new LogicException( 'getDefinitionSummary must call parent method' );
749
				}
750
				$str = json_encode( $summary );
751
752
				$mtime = $this->getModifiedTime( $context );
753
				if ( $mtime !== null ) {
754
					// Support: MediaWiki 1.25 and earlier
755
					$str .= strval( $mtime );
756
				}
757
758
				$mhash = $this->getModifiedHash( $context );
759
				if ( $mhash !== null ) {
760
					// Support: MediaWiki 1.25 and earlier
761
					$str .= strval( $mhash );
762
				}
763
			}
764
765
			$this->versionHash[$contextHash] = ResourceLoader::makeHash( $str );
766
		}
767
		return $this->versionHash[$contextHash];
768
	}
769
770
	/**
771
	 * Whether to generate version hash based on module content.
772
	 *
773
	 * If a module requires database or file system access to build the module
774
	 * content, consider disabling this in favour of manually tracking relevant
775
	 * aspects in getDefinitionSummary(). See getVersionHash() for how this is used.
776
	 *
777
	 * @return bool
778
	 */
779
	public function enableModuleContentVersion() {
780
		return false;
781
	}
782
783
	/**
784
	 * Get the definition summary for this module.
785
	 *
786
	 * This is the method subclasses are recommended to use to track values in their
787
	 * version hash. Call this in getVersionHash() and pass it to e.g. json_encode.
788
	 *
789
	 * Subclasses must call the parent getDefinitionSummary() and build on that.
790
	 * It is recommended that each subclass appends its own new array. This prevents
791
	 * clashes or accidental overwrites of existing keys and gives each subclass
792
	 * its own scope for simple array keys.
793
	 *
794
	 * @code
795
	 *     $summary = parent::getDefinitionSummary( $context );
796
	 *     $summary[] = [
797
	 *         'foo' => 123,
798
	 *         'bar' => 'quux',
799
	 *     ];
800
	 *     return $summary;
801
	 * @endcode
802
	 *
803
	 * Return an array containing values from all significant properties of this
804
	 * module's definition.
805
	 *
806
	 * Be careful not to normalise too much. Especially preserve the order of things
807
	 * that carry significance in getScript and getStyles (T39812).
808
	 *
809
	 * Avoid including things that are insiginificant (e.g. order of message keys is
810
	 * insignificant and should be sorted to avoid unnecessary cache invalidation).
811
	 *
812
	 * This data structure must exclusively contain arrays and scalars as values (avoid
813
	 * object instances) to allow simple serialisation using json_encode.
814
	 *
815
	 * If modules have a hash or timestamp from another source, that may be incuded as-is.
816
	 *
817
	 * A number of utility methods are available to help you gather data. These are not
818
	 * called by default and must be included by the subclass' getDefinitionSummary().
819
	 *
820
	 * - getMessageBlob()
821
	 *
822
	 * @since 1.23
823
	 * @param ResourceLoaderContext $context
824
	 * @return array|null
825
	 */
826
	public function getDefinitionSummary( ResourceLoaderContext $context ) {
827
		return [
828
			'_class' => get_class( $this ),
829
			'_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ),
830
		];
831
	}
832
833
	/**
834
	 * Get this module's last modification timestamp for a given context.
835
	 *
836
	 * @deprecated since 1.26 Use getDefinitionSummary() instead
837
	 * @param ResourceLoaderContext $context Context object
838
	 * @return int|null UNIX timestamp
839
	 */
840
	public function getModifiedTime( ResourceLoaderContext $context ) {
841
		return null;
842
	}
843
844
	/**
845
	 * Helper method for providing a version hash to getVersionHash().
846
	 *
847
	 * @deprecated since 1.26 Use getDefinitionSummary() instead
848
	 * @param ResourceLoaderContext $context
849
	 * @return string|null Hash
850
	 */
851
	public function getModifiedHash( ResourceLoaderContext $context ) {
852
		return null;
853
	}
854
855
	/**
856
	 * Back-compat dummy for old subclass implementations of getModifiedTime().
857
	 *
858
	 * This method used to use ObjectCache to track when a hash was first seen. That principle
859
	 * stems from a time that ResourceLoader could only identify module versions by timestamp.
860
	 * That is no longer the case. Use getDefinitionSummary() directly.
861
	 *
862
	 * @deprecated since 1.26 Superseded by getVersionHash()
863
	 * @param ResourceLoaderContext $context
864
	 * @return int UNIX timestamp
865
	 */
866
	public function getHashMtime( ResourceLoaderContext $context ) {
867
		if ( !is_string( $this->getModifiedHash( $context ) ) ) {
868
			return 1;
869
		}
870
		// Dummy that is > 1
871
		return 2;
872
	}
873
874
	/**
875
	 * Back-compat dummy for old subclass implementations of getModifiedTime().
876
	 *
877
	 * @since 1.23
878
	 * @deprecated since 1.26 Superseded by getVersionHash()
879
	 * @param ResourceLoaderContext $context
880
	 * @return int UNIX timestamp
881
	 */
882
	public function getDefinitionMtime( ResourceLoaderContext $context ) {
883
		if ( $this->getDefinitionSummary( $context ) === null ) {
884
			return 1;
885
		}
886
		// Dummy that is > 1
887
		return 2;
888
	}
889
890
	/**
891
	 * Check whether this module is known to be empty. If a child class
892
	 * has an easy and cheap way to determine that this module is
893
	 * definitely going to be empty, it should override this method to
894
	 * return true in that case. Callers may optimize the request for this
895
	 * module away if this function returns true.
896
	 * @param ResourceLoaderContext $context
897
	 * @return bool
898
	 */
899
	public function isKnownEmpty( ResourceLoaderContext $context ) {
900
		return false;
901
	}
902
903
	/** @var JSParser Lazy-initialized; use self::javaScriptParser() */
904
	private static $jsParser;
905
	private static $parseCacheVersion = 1;
906
907
	/**
908
	 * Validate a given script file; if valid returns the original source.
909
	 * If invalid, returns replacement JS source that throws an exception.
910
	 *
911
	 * @param string $fileName
912
	 * @param string $contents
913
	 * @return string JS with the original, or a replacement error
914
	 */
915
	protected function validateScriptFile( $fileName, $contents ) {
916
		if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) {
917
			// Try for cache hit
918
			$cache = ObjectCache::getMainWANInstance();
919
			$key = $cache->makeKey(
920
				'resourceloader',
921
				'jsparse',
922
				self::$parseCacheVersion,
923
				md5( $contents )
924
			);
925
			$cacheEntry = $cache->get( $key );
926
			if ( is_string( $cacheEntry ) ) {
927
				return $cacheEntry;
928
			}
929
930
			$parser = self::javaScriptParser();
931
			try {
932
				$parser->parse( $contents, $fileName, 1 );
933
				$result = $contents;
934
			} catch ( Exception $e ) {
935
				// We'll save this to cache to avoid having to validate broken JS over and over...
936
				$err = $e->getMessage();
937
				$result = "mw.log.error(" .
938
					Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
939
			}
940
941
			$cache->set( $key, $result );
942
			return $result;
943
		} else {
944
			return $contents;
945
		}
946
	}
947
948
	/**
949
	 * @return JSParser
950
	 */
951
	protected static function javaScriptParser() {
952
		if ( !self::$jsParser ) {
953
			self::$jsParser = new JSParser();
954
		}
955
		return self::$jsParser;
956
	}
957
958
	/**
959
	 * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist.
960
	 * Defaults to 1.
961
	 *
962
	 * @param string $filePath File path
963
	 * @return int UNIX timestamp
964
	 */
965
	protected static function safeFilemtime( $filePath ) {
966
		MediaWiki\suppressWarnings();
967
		$mtime = filemtime( $filePath ) ?: 1;
968
		MediaWiki\restoreWarnings();
969
		return $mtime;
970
	}
971
972
	/**
973
	 * Compute a non-cryptographic string hash of a file's contents.
974
	 * If the file does not exist or cannot be read, returns an empty string.
975
	 *
976
	 * @since 1.26 Uses MD4 instead of SHA1.
977
	 * @param string $filePath File path
978
	 * @return string Hash
979
	 */
980
	protected static function safeFileHash( $filePath ) {
981
		return FileContentsHasher::getFileContentsHash( $filePath );
982
	}
983
}
984