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.

maintenance/convertExtensionToRegistration.php (2 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
0 ignored issues
show
Coding Style Compatibility introduced by
For compatibility and reusability of your code, PSR1 recommends that a file should introduce either new symbols (like classes, functions, etc.) or have side-effects (like outputting something, or including other files), but not both at the same time. The first symbol is defined on line 5 and the first side effect is on line 3.

The PSR-1: Basic Coding Standard recommends that a file should either introduce new symbols, that is classes, functions, constants or similar, or have side effects. Side effects are anything that executes logic, like for example printing output, changing ini settings or writing to a file.

The idea behind this recommendation is that merely auto-loading a class should not change the state of an application. It also promotes a cleaner style of programming and makes your code less prone to errors, because the logic is not spread out all over the place.

To learn more about the PSR-1, please see the PHP-FIG site on the PSR-1.

Loading history...
2
3
require_once __DIR__ . '/Maintenance.php';
4
5
class ConvertExtensionToRegistration extends Maintenance {
6
7
	protected $custom = [
8
		'MessagesDirs' => 'handleMessagesDirs',
9
		'ExtensionMessagesFiles' => 'handleExtensionMessagesFiles',
10
		'AutoloadClasses' => 'removeAbsolutePath',
11
		'ExtensionCredits' => 'handleCredits',
12
		'ResourceModules' => 'handleResourceModules',
13
		'ResourceModuleSkinStyles' => 'handleResourceModules',
14
		'Hooks' => 'handleHooks',
15
		'ExtensionFunctions' => 'handleExtensionFunctions',
16
		'ParserTestFiles' => 'removeAbsolutePath',
17
	];
18
19
	/**
20
	 * Things that were formerly globals and should still be converted
21
	 *
22
	 * @var array
23
	 */
24
	protected $formerGlobals = [
25
		'TrackingCategories',
26
	];
27
28
	/**
29
	 * No longer supported globals (with reason) should not be converted and emit a warning
30
	 *
31
	 * @var array
32
	 */
33
	protected $noLongerSupportedGlobals = [
34
		'SpecialPageGroups' => 'deprecated', // Deprecated 1.21, removed in 1.26
35
	];
36
37
	/**
38
	 * Keys that should be put at the top of the generated JSON file (T86608)
39
	 *
40
	 * @var array
41
	 */
42
	protected $promote = [
43
		'name',
44
		'namemsg',
45
		'version',
46
		'author',
47
		'url',
48
		'description',
49
		'descriptionmsg',
50
		'license-name',
51
		'type',
52
	];
53
54
	private $json, $dir, $hasWarning = false;
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
55
56 View Code Duplication
	public function __construct() {
57
		parent::__construct();
58
		$this->addDescription( 'Converts extension entry points to the new JSON registration format' );
59
		$this->addArg( 'path', 'Location to the PHP entry point you wish to convert',
60
			/* $required = */ true );
61
		$this->addOption( 'skin', 'Whether to write to skin.json', false, false );
62
		$this->addOption( 'config-prefix', 'Custom prefix for configuration settings', false, true );
63
	}
64
65
	protected function getAllGlobals() {
66
		$processor = new ReflectionClass( 'ExtensionProcessor' );
67
		$settings = $processor->getProperty( 'globalSettings' );
68
		$settings->setAccessible( true );
69
		return array_merge( $settings->getValue(), $this->formerGlobals );
70
	}
71
72
	public function execute() {
73
		// Extensions will do stuff like $wgResourceModules += array(...) which is a
74
		// fatal unless an array is already set. So set an empty value.
75
		// And use the weird $__settings name to avoid any conflicts
76
		// with real poorly named settings.
77
		$__settings = array_merge( $this->getAllGlobals(), array_keys( $this->custom ) );
78
		foreach ( $__settings as $var ) {
79
			$var = 'wg' . $var;
80
			$$var = [];
81
		}
82
		unset( $var );
83
		$arg = $this->getArg( 0 );
84
		if ( !is_file( $arg ) ) {
85
			$this->error( "$arg is not a file.", true );
86
		}
87
		require $arg;
88
		unset( $arg );
89
		// Try not to create any local variables before this line
90
		$vars = get_defined_vars();
91
		unset( $vars['this'] );
92
		unset( $vars['__settings'] );
93
		$this->dir = dirname( realpath( $this->getArg( 0 ) ) );
94
		$this->json = [];
95
		$globalSettings = $this->getAllGlobals();
96
		$configPrefix = $this->getOption( 'config-prefix', 'wg' );
97
		if ( $configPrefix !== 'wg' ) {
98
			$this->json['config']['_prefix'] = $configPrefix;
99
		}
100
		foreach ( $vars as $name => $value ) {
101
			$realName = substr( $name, 2 ); // Strip 'wg'
102
			if ( $realName === false ) {
103
				continue;
104
			}
105
106
			// If it's an empty array that we likely set, skip it
107
			if ( is_array( $value ) && count( $value ) === 0 && in_array( $realName, $__settings ) ) {
108
				continue;
109
			}
110
111
			if ( isset( $this->custom[$realName] ) ) {
112
				call_user_func_array( [ $this, $this->custom[$realName] ],
113
					[ $realName, $value, $vars ] );
114
			} elseif ( in_array( $realName, $globalSettings ) ) {
115
				$this->json[$realName] = $value;
116
			} elseif ( array_key_exists( $realName, $this->noLongerSupportedGlobals ) ) {
117
				$this->output( 'Warning: Skipped global "' . $name . '" (' .
118
					$this->noLongerSupportedGlobals[$realName] . '). ' .
119
					"Please update the entry point before convert to registration.\n" );
120
				$this->hasWarning = true;
121
			} elseif ( strpos( $name, $configPrefix ) === 0 ) {
122
				// Most likely a config setting
123
				$this->json['config'][substr( $name, strlen( $configPrefix ) )] = [ 'value' => $value ];
124
			} elseif ( $configPrefix !== 'wg' && strpos( $name, 'wg' ) === 0 ) {
125
				// Warn about this
126
				$this->output( 'Warning: Skipped global "' . $name . '" (' .
127
					'config prefix is "' . $configPrefix . '"). ' .
128
					"Please check that this setting isn't needed.\n" );
129
			}
130
		}
131
132
		// check, if the extension requires composer libraries
133
		if ( $this->needsComposerAutoloader( dirname( $this->getArg( 0 ) ) ) ) {
134
			// set the load composer autoloader automatically property
135
			$this->output( "Detected composer dependencies, setting 'load_composer_autoloader' to true.\n" );
136
			$this->json['load_composer_autoloader'] = true;
137
		}
138
139
		// Move some keys to the top
140
		$out = [];
141
		foreach ( $this->promote as $key ) {
142 View Code Duplication
			if ( isset( $this->json[$key] ) ) {
143
				$out[$key] = $this->json[$key];
144
				unset( $this->json[$key] );
145
			}
146
		}
147
		$out += $this->json;
148
		// Put this at the bottom
149
		$out['manifest_version'] = ExtensionRegistry::MANIFEST_VERSION;
150
		$type = $this->hasOption( 'skin' ) ? 'skin' : 'extension';
151
		$fname = "{$this->dir}/$type.json";
152
		$prettyJSON = FormatJson::encode( $out, "\t", FormatJson::ALL_OK );
153
		file_put_contents( $fname, $prettyJSON . "\n" );
154
		$this->output( "Wrote output to $fname.\n" );
155
		if ( $this->hasWarning ) {
156
			$this->output( "Found warnings! Please resolve the warnings and rerun this script.\n" );
157
		}
158
	}
159
160
	protected function handleExtensionFunctions( $realName, $value ) {
161
		foreach ( $value as $func ) {
162
			if ( $func instanceof Closure ) {
163
				$this->error( "Error: Closures cannot be converted to JSON. " .
164
					"Please move your extension function somewhere else.", 1
165
				);
166
			}
167
			// check if $func exists in the global scope
168
			if ( function_exists( $func ) ) {
169
				$this->error( "Error: Global functions cannot be converted to JSON. " .
170
					"Please move your extension function ($func) into a class.", 1
171
				);
172
			}
173
		}
174
175
		$this->json[$realName] = $value;
176
	}
177
178 View Code Duplication
	protected function handleMessagesDirs( $realName, $value ) {
179
		foreach ( $value as $key => $dirs ) {
180
			foreach ( (array)$dirs as $dir ) {
181
				$this->json[$realName][$key][] = $this->stripPath( $dir, $this->dir );
182
			}
183
		}
184
	}
185
186
	protected function handleExtensionMessagesFiles( $realName, $value, $vars ) {
187
		foreach ( $value as $key => $file ) {
188
			$strippedFile = $this->stripPath( $file, $this->dir );
189
			if ( isset( $vars['wgMessagesDirs'][$key] ) ) {
190
				$this->output(
191
					"Note: Ignoring PHP shim $strippedFile. " .
192
					"If your extension no longer supports versions of MediaWiki " .
193
					"older than 1.23.0, you can safely delete it.\n"
194
				);
195
			} else {
196
				$this->json[$realName][$key] = $strippedFile;
197
			}
198
		}
199
	}
200
201
	private function stripPath( $val, $dir ) {
202
		if ( $val === $dir ) {
203
			$val = '';
204
		} elseif ( strpos( $val, $dir ) === 0 ) {
205
			// +1 is for the trailing / that won't be in $this->dir
206
			$val = substr( $val, strlen( $dir ) + 1 );
207
		}
208
209
		return $val;
210
	}
211
212 View Code Duplication
	protected function removeAbsolutePath( $realName, $value ) {
213
		$out = [];
214
		foreach ( $value as $key => $val ) {
215
			$out[$key] = $this->stripPath( $val, $this->dir );
216
		}
217
		$this->json[$realName] = $out;
218
	}
219
220
	protected function handleCredits( $realName, $value ) {
221
		$keys = array_keys( $value );
222
		$this->json['type'] = $keys[0];
223
		$values = array_values( $value );
224
		foreach ( $values[0][0] as $name => $val ) {
225
			if ( $name !== 'path' ) {
226
				$this->json[$name] = $val;
227
			}
228
		}
229
	}
230
231
	public function handleHooks( $realName, $value ) {
232
		foreach ( $value as $hookName => &$handlers ) {
233
			if ( $hookName === 'UnitTestsList' ) {
234
				$this->output( "Note: the UnitTestsList hook is no longer necessary as " .
235
					"long as your tests are located in the \"tests/phpunit/\" directory. " .
236
					"Please see <https://www.mediawiki.org/wiki/Manual:PHP_unit_testing/" .
237
					"Writing_unit_tests_for_extensions#Register_your_tests> for more details.\n"
238
				);
239
			}
240
			foreach ( $handlers as $func ) {
241
				if ( $func instanceof Closure ) {
242
					$this->error( "Error: Closures cannot be converted to JSON. " .
243
						"Please move the handler for $hookName somewhere else.", 1
244
					);
245
				}
246
				// Check if $func exists in the global scope
247
				if ( function_exists( $func ) ) {
248
					$this->error( "Error: Global functions cannot be converted to JSON. " .
249
						"Please move the handler for $hookName inside a class.", 1
250
					);
251
				}
252
			}
253
			if ( count( $handlers ) === 1 ) {
254
				$handlers = $handlers[0];
255
			}
256
		}
257
		$this->json[$realName] = $value;
258
	}
259
260
	protected function handleResourceModules( $realName, $value ) {
261
		$defaults = [];
262
		$remote = $this->hasOption( 'skin' ) ? 'remoteSkinPath' : 'remoteExtPath';
263
		foreach ( $value as $name => $data ) {
264
			if ( isset( $data['localBasePath'] ) ) {
265
				$data['localBasePath'] = $this->stripPath( $data['localBasePath'], $this->dir );
266
				if ( !$defaults ) {
267
					$defaults['localBasePath'] = $data['localBasePath'];
268
					unset( $data['localBasePath'] );
269
					if ( isset( $data[$remote] ) ) {
270
						$defaults[$remote] = $data[$remote];
271
						unset( $data[$remote] );
272
					}
273
				} else {
274
					if ( $data['localBasePath'] === $defaults['localBasePath'] ) {
275
						unset( $data['localBasePath'] );
276
					}
277
					if ( isset( $data[$remote] ) && isset( $defaults[$remote] )
278
						&& $data[$remote] === $defaults[$remote]
279
					) {
280
						unset( $data[$remote] );
281
					}
282
				}
283
			}
284
285
			$this->json[$realName][$name] = $data;
286
		}
287
		if ( $defaults ) {
288
			$this->json['ResourceFileModulePaths'] = $defaults;
289
		}
290
	}
291
292
	protected function needsComposerAutoloader( $path ) {
293
		$path .= '/composer.json';
294
		if ( file_exists( $path ) ) {
295
			// assume, that the composer.json file is in the root of the extension path
296
			$composerJson = new ComposerJson( $path );
297
			// check, if there are some dependencies in the require section
298
			if ( $composerJson->getRequiredDependencies() ) {
299
				return true;
300
			}
301
		}
302
		return false;
303
	}
304
}
305
306
$maintClass = 'ConvertExtensionToRegistration';
307
require_once RUN_MAINTENANCE_IF_MAIN;
308