ConvertExtensionToRegistration::getAllGlobals()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
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
Coding Style introduced by
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