Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
86.05% |
296 / 344 |
|
55.56% |
15 / 27 |
CRAP | |
0.00% |
0 / 1 |
ExtensionProcessor | |
86.05% |
296 / 344 |
|
55.56% |
15 / 27 |
223.96 | |
0.00% |
0 / 1 |
extractInfoFromFile | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
extractInfo | |
84.78% |
39 / 46 |
|
0.00% |
0 / 1 |
15.79 | |||
extractAttributes | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getExtractedInfo | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
6 | |||
getRequirements | |
100.00% |
43 / 43 |
|
100.00% |
1 / 1 |
13 | |||
setArrayHookHandler | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
7 | |||
setStringHookHandler | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
extractHooks | |
61.11% |
11 / 18 |
|
0.00% |
0 / 1 |
13.76 | |||
extractNamespaces | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
18.63 | |||
extractResourceLoaderModules | |
70.97% |
22 / 31 |
|
0.00% |
0 / 1 |
22.26 | |||
extractExtensionMessagesFiles | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
extractMessagesDirs | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
extractTranslationAliasesDirs | |
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
5.67 | |||
extractSkins | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
extractImplicitRights | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
extractSkinImportPaths | |
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
5.67 | |||
extractCredits | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
4 | |||
extractForeignResourcesDir | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
3.14 | |||
extractConfig1 | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
5 | |||
applyPath | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
extractConfig2 | |
87.50% |
14 / 16 |
|
0.00% |
0 / 1 |
9.16 | |||
addConfigGlobal | |
58.33% |
7 / 12 |
|
0.00% |
0 / 1 |
5.16 | |||
extractPathBasedGlobal | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
storeToArrayRecursive | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
storeToArray | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
3.07 | |||
getExtractedAutoloadInfo | |
60.00% |
6 / 10 |
|
0.00% |
0 / 1 |
3.58 | |||
extractAutoload | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
8 |
1 | <?php |
2 | |
3 | use MediaWiki\MainConfigNames; |
4 | use MediaWiki\ResourceLoader\FilePath; |
5 | |
6 | /** |
7 | * Load extension manifests and then aggregate their contents. |
8 | * |
9 | * @ingroup ExtensionRegistry |
10 | * @newable since 1.39 |
11 | */ |
12 | class ExtensionProcessor implements Processor { |
13 | |
14 | /** |
15 | * Keys that should be set to $GLOBALS |
16 | * |
17 | * @var array |
18 | */ |
19 | protected static $globalSettings = [ |
20 | MainConfigNames::ActionFilteredLogs, |
21 | MainConfigNames::Actions, |
22 | MainConfigNames::AddGroups, |
23 | MainConfigNames::APIFormatModules, |
24 | MainConfigNames::APIListModules, |
25 | MainConfigNames::APIMetaModules, |
26 | MainConfigNames::APIModules, |
27 | MainConfigNames::APIPropModules, |
28 | MainConfigNames::AuthManagerAutoConfig, |
29 | MainConfigNames::AvailableRights, |
30 | MainConfigNames::CentralIdLookupProviders, |
31 | MainConfigNames::ChangeCredentialsBlacklist, |
32 | MainConfigNames::ConditionalUserOptions, |
33 | MainConfigNames::ConfigRegistry, |
34 | MainConfigNames::ContentHandlers, |
35 | MainConfigNames::DefaultUserOptions, |
36 | MainConfigNames::ExtensionEntryPointListFiles, |
37 | MainConfigNames::ExtensionFunctions, |
38 | MainConfigNames::FeedClasses, |
39 | MainConfigNames::FileExtensions, |
40 | MainConfigNames::FilterLogTypes, |
41 | MainConfigNames::GrantPermissionGroups, |
42 | MainConfigNames::GrantPermissions, |
43 | MainConfigNames::GrantRiskGroups, |
44 | MainConfigNames::GroupPermissions, |
45 | MainConfigNames::GroupsAddToSelf, |
46 | MainConfigNames::GroupsRemoveFromSelf, |
47 | MainConfigNames::HiddenPrefs, |
48 | MainConfigNames::ImplicitGroups, |
49 | MainConfigNames::JobClasses, |
50 | MainConfigNames::LogActions, |
51 | MainConfigNames::LogActionsHandlers, |
52 | MainConfigNames::LogHeaders, |
53 | MainConfigNames::LogNames, |
54 | MainConfigNames::LogRestrictions, |
55 | MainConfigNames::LogTypes, |
56 | MainConfigNames::MediaHandlers, |
57 | MainConfigNames::PasswordPolicy, |
58 | MainConfigNames::PrivilegedGroups, |
59 | MainConfigNames::RateLimits, |
60 | MainConfigNames::RawHtmlMessages, |
61 | MainConfigNames::ReauthenticateTime, |
62 | MainConfigNames::RecentChangesFlags, |
63 | MainConfigNames::RemoveCredentialsBlacklist, |
64 | MainConfigNames::RemoveGroups, |
65 | MainConfigNames::ResourceLoaderSources, |
66 | MainConfigNames::RevokePermissions, |
67 | MainConfigNames::SessionProviders, |
68 | MainConfigNames::SpecialPages, |
69 | MainConfigNames::UserRegistrationProviders, |
70 | ]; |
71 | |
72 | /** |
73 | * Top-level attributes that come from MW core |
74 | * |
75 | * @var string[] |
76 | */ |
77 | protected const CORE_ATTRIBS = [ |
78 | 'ParsoidModules', |
79 | 'RestRoutes', |
80 | 'SkinOOUIThemes', |
81 | 'SkinCodexThemes', |
82 | 'SearchMappings', |
83 | 'TrackingCategories', |
84 | 'LateJSConfigVarNames', |
85 | 'TempUserSerialProviders', |
86 | 'TempUserSerialMappings', |
87 | 'DatabaseVirtualDomains', |
88 | ]; |
89 | |
90 | /** |
91 | * Mapping of global settings to their specific merge strategies. |
92 | * |
93 | * @see ExtensionRegistry::exportExtractedData |
94 | * @see getExtractedInfo |
95 | * @var array |
96 | */ |
97 | protected const MERGE_STRATEGIES = [ |
98 | 'wgAuthManagerAutoConfig' => 'array_plus_2d', |
99 | 'wgCapitalLinkOverrides' => 'array_plus', |
100 | 'wgExtraGenderNamespaces' => 'array_plus', |
101 | 'wgGrantPermissions' => 'array_plus_2d', |
102 | 'wgGroupPermissions' => 'array_plus_2d', |
103 | 'wgHooks' => 'array_merge_recursive', |
104 | 'wgNamespaceContentModels' => 'array_plus', |
105 | 'wgNamespaceProtection' => 'array_plus', |
106 | 'wgNamespacesWithSubpages' => 'array_plus', |
107 | 'wgPasswordPolicy' => 'array_merge_recursive', |
108 | 'wgRateLimits' => 'array_plus_2d', |
109 | 'wgRevokePermissions' => 'array_plus_2d', |
110 | ]; |
111 | |
112 | /** |
113 | * Keys that are part of the extension credits |
114 | * |
115 | * @var array |
116 | */ |
117 | protected const CREDIT_ATTRIBS = [ |
118 | 'type', |
119 | 'author', |
120 | 'description', |
121 | 'descriptionmsg', |
122 | 'license-name', |
123 | 'name', |
124 | 'namemsg', |
125 | 'url', |
126 | 'version', |
127 | ]; |
128 | |
129 | /** |
130 | * Things that are not 'attributes', and are not in |
131 | * $globalSettings or CREDIT_ATTRIBS. |
132 | * |
133 | * @var array |
134 | */ |
135 | protected const NOT_ATTRIBS = [ |
136 | 'callback', |
137 | 'config', |
138 | 'config_prefix', |
139 | 'load_composer_autoloader', |
140 | 'manifest_version', |
141 | 'namespaces', |
142 | 'requires', |
143 | 'AutoloadClasses', |
144 | 'AutoloadNamespaces', |
145 | 'ExtensionMessagesFiles', |
146 | 'TranslationAliasesDirs', |
147 | 'ForeignResourcesDir', |
148 | 'Hooks', |
149 | 'MessagePosterModule', |
150 | 'MessagesDirs', |
151 | 'OOUIThemePaths', |
152 | 'QUnitTestModule', |
153 | 'ResourceFileModulePaths', |
154 | 'ResourceModuleSkinStyles', |
155 | 'ResourceModules', |
156 | 'ServiceWiringFiles', |
157 | ]; |
158 | |
159 | /** |
160 | * Stuff that is going to be set to $GLOBALS |
161 | * |
162 | * Some keys are pre-set to arrays, so we can += to them |
163 | * |
164 | * @var array |
165 | */ |
166 | protected $globals = [ |
167 | 'wgExtensionMessagesFiles' => [], |
168 | 'wgMessagesDirs' => [], |
169 | 'TranslationAliasesDirs' => [], |
170 | ]; |
171 | |
172 | /** |
173 | * Things that should be define()'d |
174 | * |
175 | * @var array |
176 | */ |
177 | protected $defines = []; |
178 | |
179 | /** |
180 | * Things to be called once the registration of these extensions is done |
181 | * |
182 | * Keyed by the name of the extension that it belongs to |
183 | * |
184 | * @var callable[] |
185 | */ |
186 | protected $callbacks = []; |
187 | |
188 | /** |
189 | * @var array |
190 | */ |
191 | protected $credits = []; |
192 | |
193 | /** |
194 | * Autoloader information. |
195 | * Each element is an array of strings. |
196 | * 'files' is just a list, 'classes' and 'namespaces' are associative. |
197 | * |
198 | * @var string[][] |
199 | */ |
200 | protected $autoload = [ |
201 | 'files' => [], |
202 | 'classes' => [], |
203 | 'namespaces' => [], |
204 | ]; |
205 | |
206 | /** |
207 | * Autoloader information for development. |
208 | * Same structure as $autoload. |
209 | * |
210 | * @var string[][] |
211 | */ |
212 | protected $autoloadDev = [ |
213 | 'files' => [], |
214 | 'classes' => [], |
215 | 'namespaces' => [], |
216 | ]; |
217 | |
218 | /** |
219 | * Anything else in the $info that hasn't |
220 | * already been processed |
221 | * |
222 | * @var array |
223 | */ |
224 | protected $attributes = []; |
225 | |
226 | /** |
227 | * Extension attributes, keyed by name => |
228 | * settings. |
229 | * |
230 | * @var array |
231 | */ |
232 | protected $extAttributes = []; |
233 | |
234 | /** |
235 | * Extracts extension info from the given JSON file. |
236 | * |
237 | * @param string $path |
238 | * |
239 | * @return void |
240 | */ |
241 | public function extractInfoFromFile( string $path ) { |
242 | $json = file_get_contents( $path ); |
243 | $info = json_decode( $json, true ); |
244 | |
245 | if ( !$info ) { |
246 | throw new RuntimeException( "Failed to load JSON data from $path" ); |
247 | } |
248 | |
249 | $this->extractInfo( $path, $info, $info['manifest_version'] ); |
250 | } |
251 | |
252 | /** |
253 | * @param string $path |
254 | * @param array $info |
255 | * @param int $version manifest_version for info |
256 | */ |
257 | public function extractInfo( $path, array $info, $version ) { |
258 | $dir = dirname( $path ); |
259 | $this->extractHooks( $info, $path ); |
260 | $this->extractExtensionMessagesFiles( $dir, $info ); |
261 | $this->extractMessagesDirs( $dir, $info ); |
262 | $this->extractTranslationAliasesDirs( $dir, $info ); |
263 | $this->extractSkins( $dir, $info ); |
264 | $this->extractSkinImportPaths( $dir, $info ); |
265 | $this->extractNamespaces( $info ); |
266 | $this->extractImplicitRights( $info ); |
267 | $this->extractResourceLoaderModules( $dir, $info ); |
268 | if ( isset( $info['ServiceWiringFiles'] ) ) { |
269 | $this->extractPathBasedGlobal( |
270 | 'wgServiceWiringFiles', |
271 | $dir, |
272 | $info['ServiceWiringFiles'] |
273 | ); |
274 | } |
275 | $name = $this->extractCredits( $path, $info ); |
276 | if ( isset( $info['callback'] ) ) { |
277 | $this->callbacks[$name] = $info['callback']; |
278 | } |
279 | |
280 | $this->extractAutoload( $info, $dir ); |
281 | |
282 | // config should be after all core globals are extracted, |
283 | // so duplicate setting detection will work fully |
284 | if ( $version >= 2 ) { |
285 | $this->extractConfig2( $info, $dir ); |
286 | } else { |
287 | // $version === 1 |
288 | $this->extractConfig1( $info ); |
289 | } |
290 | |
291 | // Record the extension name in the ParsoidModules property |
292 | if ( isset( $info['ParsoidModules'] ) ) { |
293 | foreach ( $info['ParsoidModules'] as &$module ) { |
294 | if ( is_string( $module ) ) { |
295 | $className = $module; |
296 | $module = [ |
297 | 'class' => $className, |
298 | ]; |
299 | } |
300 | $module['name'] = $name; |
301 | } |
302 | } |
303 | |
304 | $this->extractForeignResourcesDir( $info, $name, $dir ); |
305 | |
306 | if ( $version >= 2 ) { |
307 | $this->extractAttributes( $path, $info ); |
308 | } |
309 | |
310 | foreach ( $info as $key => $val ) { |
311 | // If it's a global setting, |
312 | if ( in_array( $key, self::$globalSettings ) ) { |
313 | $this->storeToArrayRecursive( $path, "wg$key", $val, $this->globals ); |
314 | continue; |
315 | } |
316 | // Ignore anything that starts with a @ |
317 | if ( $key[0] === '@' ) { |
318 | continue; |
319 | } |
320 | |
321 | if ( $version >= 2 ) { |
322 | // Only allowed attributes are set |
323 | if ( in_array( $key, self::CORE_ATTRIBS ) ) { |
324 | $this->storeToArray( $path, $key, $val, $this->attributes ); |
325 | } |
326 | } else { |
327 | // version === 1 |
328 | if ( !in_array( $key, self::NOT_ATTRIBS ) |
329 | && !in_array( $key, self::CREDIT_ATTRIBS ) |
330 | ) { |
331 | // If it's not disallowed, it's an attribute |
332 | $this->storeToArrayRecursive( $path, $key, $val, $this->attributes ); |
333 | } |
334 | } |
335 | } |
336 | } |
337 | |
338 | /** |
339 | * @param string $path |
340 | * @param array $info |
341 | */ |
342 | protected function extractAttributes( $path, array $info ) { |
343 | if ( isset( $info['attributes'] ) ) { |
344 | foreach ( $info['attributes'] as $extName => $value ) { |
345 | $this->storeToArrayRecursive( $path, $extName, $value, $this->extAttributes ); |
346 | } |
347 | } |
348 | } |
349 | |
350 | public function getExtractedInfo( bool $includeDev = false ) { |
351 | // Make sure the merge strategies are set |
352 | foreach ( $this->globals as $key => $val ) { |
353 | if ( isset( self::MERGE_STRATEGIES[$key] ) ) { |
354 | $this->globals[$key][ExtensionRegistry::MERGE_STRATEGY] = self::MERGE_STRATEGIES[$key]; |
355 | } |
356 | } |
357 | |
358 | // Merge $this->extAttributes into $this->attributes depending on what is loaded |
359 | foreach ( $this->extAttributes as $extName => $value ) { |
360 | // Only set the attribute if $extName is loaded (and hence present in credits) |
361 | if ( isset( $this->credits[$extName] ) ) { |
362 | foreach ( $value as $attrName => $attrValue ) { |
363 | $this->storeToArrayRecursive( |
364 | '', // Don't provide a path since it's impossible to generate an error here |
365 | $extName . $attrName, |
366 | $attrValue, |
367 | $this->attributes |
368 | ); |
369 | } |
370 | unset( $this->extAttributes[$extName] ); |
371 | } |
372 | } |
373 | |
374 | $autoload = $this->getExtractedAutoloadInfo( $includeDev ); |
375 | return [ |
376 | 'globals' => $this->globals, |
377 | 'defines' => $this->defines, |
378 | 'callbacks' => $this->callbacks, |
379 | 'credits' => $this->credits, |
380 | 'attributes' => $this->attributes, |
381 | 'autoloaderPaths' => $autoload['files'], |
382 | 'autoloaderClasses' => $autoload['classes'], |
383 | 'autoloaderNS' => $autoload['namespaces'], |
384 | ]; |
385 | } |
386 | |
387 | public function getRequirements( array $info, $includeDev ) { |
388 | // Quick shortcuts |
389 | if ( !$includeDev || !isset( $info['dev-requires'] ) ) { |
390 | return $info['requires'] ?? []; |
391 | } |
392 | |
393 | if ( !isset( $info['requires'] ) ) { |
394 | return $info['dev-requires'] ?? []; |
395 | } |
396 | |
397 | // OK, we actually have to merge everything |
398 | $merged = []; |
399 | |
400 | // Helper that combines version requirements by |
401 | // picking the non-null if one is, or combines |
402 | // the two. Note that it is not possible for |
403 | // both inputs to be null. |
404 | $pick = static function ( $a, $b ) { |
405 | if ( $a === null ) { |
406 | return $b; |
407 | } elseif ( $b === null ) { |
408 | return $a; |
409 | } else { |
410 | return "$a $b"; |
411 | } |
412 | }; |
413 | |
414 | $req = $info['requires']; |
415 | $dev = $info['dev-requires']; |
416 | if ( isset( $req['MediaWiki'] ) || isset( $dev['MediaWiki'] ) ) { |
417 | $merged['MediaWiki'] = $pick( |
418 | $req['MediaWiki'] ?? null, |
419 | $dev['MediaWiki'] ?? null |
420 | ); |
421 | } |
422 | |
423 | $platform = array_merge( |
424 | array_keys( $req['platform'] ?? [] ), |
425 | array_keys( $dev['platform'] ?? [] ) |
426 | ); |
427 | if ( $platform ) { |
428 | foreach ( $platform as $pkey ) { |
429 | if ( $pkey === 'php' ) { |
430 | $value = $pick( |
431 | $req['platform']['php'] ?? null, |
432 | $dev['platform']['php'] ?? null |
433 | ); |
434 | } else { |
435 | // Prefer dev value, but these should be constant |
436 | // anyway (ext-* and ability-*) |
437 | $value = $dev['platform'][$pkey] ?? $req['platform'][$pkey]; |
438 | } |
439 | $merged['platform'][$pkey] = $value; |
440 | } |
441 | } |
442 | |
443 | foreach ( [ 'extensions', 'skins' ] as $thing ) { |
444 | $things = array_merge( |
445 | array_keys( $req[$thing] ?? [] ), |
446 | array_keys( $dev[$thing] ?? [] ) |
447 | ); |
448 | foreach ( $things as $name ) { |
449 | $merged[$thing][$name] = $pick( |
450 | $req[$thing][$name] ?? null, |
451 | $dev[$thing][$name] ?? null |
452 | ); |
453 | } |
454 | } |
455 | return $merged; |
456 | } |
457 | |
458 | /** |
459 | * When handler value is an array, set $wgHooks or Hooks attribute |
460 | * Could be legacy hook e.g. 'GlobalFunctionName' or non-legacy hook |
461 | * referencing a handler definition from 'HookHandler' attribute |
462 | * |
463 | * @param array $callback Handler |
464 | * @param array $hookHandlersAttr handler definitions from 'HookHandler' attribute |
465 | * @param string $name |
466 | * @param string $path extension.json file path |
467 | * @throws UnexpectedValueException |
468 | */ |
469 | private function setArrayHookHandler( |
470 | array $callback, |
471 | array $hookHandlersAttr, |
472 | string $name, |
473 | string $path |
474 | ) { |
475 | if ( isset( $callback['handler'] ) ) { |
476 | $handlerName = $callback['handler']; |
477 | $handlerDefinition = $hookHandlersAttr[$handlerName] ?? false; |
478 | if ( !$handlerDefinition ) { |
479 | throw new UnexpectedValueException( |
480 | "Missing handler definition for $name in HookHandlers attribute in $path" |
481 | ); |
482 | } |
483 | $callback['handler'] = $handlerDefinition; |
484 | $callback['extensionPath'] = $path; |
485 | $this->attributes['Hooks'][$name][] = $callback; |
486 | } else { |
487 | foreach ( $callback as $callable ) { |
488 | if ( is_array( $callable ) ) { |
489 | if ( isset( $callable['handler'] ) ) { // Non-legacy style handler |
490 | $this->setArrayHookHandler( $callable, $hookHandlersAttr, $name, $path ); |
491 | } else { // Legacy style handler array |
492 | $this->globals['wgHooks'][$name][] = $callable; |
493 | } |
494 | } elseif ( is_string( $callable ) ) { |
495 | $this->setStringHookHandler( $callable, $hookHandlersAttr, $name, $path ); |
496 | } |
497 | } |
498 | } |
499 | } |
500 | |
501 | /** |
502 | * When handler value is a string, set $wgHooks or Hooks attribute. |
503 | * Could be legacy hook e.g. 'GlobalFunctionName' or non-legacy hook |
504 | * referencing a handler definition from 'HookHandler' attribute |
505 | * |
506 | * @param string $callback Handler |
507 | * @param array $hookHandlersAttr handler definitions from 'HookHandler' attribute |
508 | * @param string $name |
509 | * @param string $path |
510 | */ |
511 | private function setStringHookHandler( |
512 | string $callback, |
513 | array $hookHandlersAttr, |
514 | string $name, |
515 | string $path |
516 | ) { |
517 | if ( isset( $hookHandlersAttr[$callback] ) ) { |
518 | $handler = [ |
519 | 'handler' => $hookHandlersAttr[$callback], |
520 | 'extensionPath' => $path |
521 | ]; |
522 | $this->attributes['Hooks'][$name][] = $handler; |
523 | } else { // legacy style handler |
524 | $this->globals['wgHooks'][$name][] = $callback; |
525 | } |
526 | } |
527 | |
528 | /** |
529 | * Extract hook information from Hooks and HookHandler attributes. |
530 | * Store hook in $wgHooks if a legacy style handler or the 'Hooks' attribute if |
531 | * a non-legacy handler |
532 | * |
533 | * @param array $info attributes and associated values from extension.json |
534 | * @param string $path path to extension.json |
535 | */ |
536 | protected function extractHooks( array $info, string $path ) { |
537 | $extName = $info['name']; |
538 | if ( isset( $info['Hooks'] ) ) { |
539 | $hookHandlersAttr = []; |
540 | foreach ( $info['HookHandlers'] ?? [] as $name => $def ) { |
541 | $hookHandlersAttr[$name] = [ 'name' => "$extName-$name" ] + $def; |
542 | } |
543 | foreach ( $info['Hooks'] as $name => $callback ) { |
544 | if ( is_string( $callback ) ) { |
545 | $this->setStringHookHandler( $callback, $hookHandlersAttr, $name, $path ); |
546 | } elseif ( is_array( $callback ) ) { |
547 | $this->setArrayHookHandler( $callback, $hookHandlersAttr, $name, $path ); |
548 | } |
549 | } |
550 | } |
551 | if ( isset( $info['DeprecatedHooks'] ) ) { |
552 | $deprecatedHooks = []; |
553 | foreach ( $info['DeprecatedHooks'] as $name => $deprecatedHookInfo ) { |
554 | $deprecatedHookInfo += [ 'component' => $extName ]; |
555 | $deprecatedHooks[$name] = $deprecatedHookInfo; |
556 | } |
557 | if ( isset( $this->attributes['DeprecatedHooks'] ) ) { |
558 | $this->attributes['DeprecatedHooks'] += $deprecatedHooks; |
559 | } else { |
560 | $this->attributes['DeprecatedHooks'] = $deprecatedHooks; |
561 | } |
562 | } |
563 | } |
564 | |
565 | /** |
566 | * Register namespaces with the appropriate global settings |
567 | * |
568 | * @param array $info |
569 | */ |
570 | protected function extractNamespaces( array $info ) { |
571 | if ( isset( $info['namespaces'] ) ) { |
572 | foreach ( $info['namespaces'] as $ns ) { |
573 | if ( defined( $ns['constant'] ) ) { |
574 | // If the namespace constant is already defined, use it. |
575 | // This allows namespace IDs to be overwritten locally. |
576 | $id = constant( $ns['constant'] ); |
577 | } else { |
578 | $id = $ns['id']; |
579 | } |
580 | $this->defines[ $ns['constant'] ] = $id; |
581 | |
582 | if ( !( isset( $ns['conditional'] ) && $ns['conditional'] ) ) { |
583 | // If it is not conditional, register it |
584 | $this->attributes['ExtensionNamespaces'][$id] = $ns['name']; |
585 | } |
586 | if ( isset( $ns['movable'] ) && !$ns['movable'] ) { |
587 | $this->attributes['ImmovableNamespaces'][] = $id; |
588 | } |
589 | if ( isset( $ns['gender'] ) ) { |
590 | $this->globals['wgExtraGenderNamespaces'][$id] = $ns['gender']; |
591 | } |
592 | if ( isset( $ns['subpages'] ) && $ns['subpages'] ) { |
593 | $this->globals['wgNamespacesWithSubpages'][$id] = true; |
594 | } |
595 | if ( isset( $ns['content'] ) && $ns['content'] ) { |
596 | $this->globals['wgContentNamespaces'][] = $id; |
597 | } |
598 | if ( isset( $ns['defaultcontentmodel'] ) ) { |
599 | $this->globals['wgNamespaceContentModels'][$id] = $ns['defaultcontentmodel']; |
600 | } |
601 | if ( isset( $ns['protection'] ) ) { |
602 | $this->globals['wgNamespaceProtection'][$id] = $ns['protection']; |
603 | } |
604 | if ( isset( $ns['capitallinkoverride'] ) ) { |
605 | $this->globals['wgCapitalLinkOverrides'][$id] = $ns['capitallinkoverride']; |
606 | } |
607 | if ( isset( $ns['includable'] ) && !$ns['includable'] ) { |
608 | $this->globals['wgNonincludableNamespaces'][] = $id; |
609 | } |
610 | } |
611 | } |
612 | } |
613 | |
614 | protected function extractResourceLoaderModules( $dir, array $info ) { |
615 | $defaultPaths = $info['ResourceFileModulePaths'] ?? false; |
616 | if ( isset( $defaultPaths['localBasePath'] ) ) { |
617 | if ( $defaultPaths['localBasePath'] === '' ) { |
618 | // Avoid double slashes (e.g. /extensions/Example//path) |
619 | $defaultPaths['localBasePath'] = $dir; |
620 | } else { |
621 | $defaultPaths['localBasePath'] = "$dir/{$defaultPaths['localBasePath']}"; |
622 | } |
623 | } |
624 | |
625 | foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) { |
626 | if ( isset( $info[$setting] ) ) { |
627 | foreach ( $info[$setting] as $name => $data ) { |
628 | if ( isset( $data['localBasePath'] ) ) { |
629 | if ( $data['localBasePath'] === '' ) { |
630 | // Avoid double slashes (e.g. /extensions/Example//path) |
631 | $data['localBasePath'] = $dir; |
632 | } else { |
633 | $data['localBasePath'] = "$dir/{$data['localBasePath']}"; |
634 | } |
635 | } |
636 | if ( $defaultPaths ) { |
637 | $data += $defaultPaths; |
638 | } |
639 | $this->attributes[$setting][$name] = $data; |
640 | } |
641 | } |
642 | } |
643 | |
644 | if ( isset( $info['QUnitTestModule'] ) ) { |
645 | $data = $info['QUnitTestModule']; |
646 | if ( isset( $data['localBasePath'] ) ) { |
647 | if ( $data['localBasePath'] === '' ) { |
648 | // Avoid double slashes (e.g. /extensions/Example//path) |
649 | $data['localBasePath'] = $dir; |
650 | } else { |
651 | $data['localBasePath'] = "$dir/{$data['localBasePath']}"; |
652 | } |
653 | } |
654 | $this->attributes['QUnitTestModules']["test.{$info['name']}"] = $data; |
655 | } |
656 | |
657 | if ( isset( $info['MessagePosterModule'] ) ) { |
658 | $data = $info['MessagePosterModule']; |
659 | $basePath = $data['localBasePath'] ?? ''; |
660 | $baseDir = $basePath === '' ? $dir : "$dir/$basePath"; |
661 | foreach ( $data['scripts'] ?? [] as $scripts ) { |
662 | $this->attributes['MessagePosterModule']['scripts'][] = |
663 | new FilePath( $scripts, $baseDir ); |
664 | } |
665 | foreach ( $data['dependencies'] ?? [] as $dependency ) { |
666 | $this->attributes['MessagePosterModule']['dependencies'][] = $dependency; |
667 | } |
668 | } |
669 | } |
670 | |
671 | protected function extractExtensionMessagesFiles( $dir, array $info ) { |
672 | if ( isset( $info['ExtensionMessagesFiles'] ) ) { |
673 | foreach ( $info['ExtensionMessagesFiles'] as &$file ) { |
674 | $file = "$dir/$file"; |
675 | } |
676 | $this->globals["wgExtensionMessagesFiles"] += $info['ExtensionMessagesFiles']; |
677 | } |
678 | } |
679 | |
680 | /** |
681 | * Set message-related settings, which need to be expanded to use |
682 | * absolute paths |
683 | * |
684 | * @param string $dir |
685 | * @param array $info |
686 | */ |
687 | protected function extractMessagesDirs( $dir, array $info ) { |
688 | if ( isset( $info['MessagesDirs'] ) ) { |
689 | foreach ( $info['MessagesDirs'] as $name => $files ) { |
690 | foreach ( (array)$files as $file ) { |
691 | $this->globals["wgMessagesDirs"][$name][] = "$dir/$file"; |
692 | } |
693 | } |
694 | } |
695 | } |
696 | |
697 | /** |
698 | * Set localization related settings, which need to be expanded to use |
699 | * absolute paths |
700 | * |
701 | * @param string $dir |
702 | * @param array $info |
703 | */ |
704 | protected function extractTranslationAliasesDirs( $dir, array $info ) { |
705 | foreach ( $info['TranslationAliasesDirs'] ?? [] as $name => $files ) { |
706 | foreach ( (array)$files as $file ) { |
707 | $this->globals['wgTranslationAliasesDirs'][$name][] = "$dir/$file"; |
708 | } |
709 | } |
710 | } |
711 | |
712 | /** |
713 | * Extract skins and handle path correction for templateDirectory. |
714 | * |
715 | * @param string $dir |
716 | * @param array $info |
717 | */ |
718 | protected function extractSkins( $dir, array $info ) { |
719 | if ( isset( $info['ValidSkinNames'] ) ) { |
720 | foreach ( $info['ValidSkinNames'] as $skinKey => $data ) { |
721 | if ( isset( $data['args'][0] ) ) { |
722 | $templateDirectory = $data['args'][0]['templateDirectory'] ?? 'templates'; |
723 | $data['args'][0]['templateDirectory'] = $dir . '/' . $templateDirectory; |
724 | } |
725 | $this->globals['wgValidSkinNames'][$skinKey] = $data; |
726 | } |
727 | } |
728 | } |
729 | |
730 | /** |
731 | * Extract any user rights that should be granted implicitly. |
732 | * |
733 | * @param array $info |
734 | */ |
735 | protected function extractImplicitRights( array $info ) { |
736 | // Rate limits are only configurable for rights that are either in wgImplicitRights |
737 | // or in wgAvailableRights. Extensions that define rate limits should not have to |
738 | // explicitly add them to wgImplicitRights as well, we can do that automatically. |
739 | |
740 | if ( isset( $info['RateLimits'] ) ) { |
741 | $rights = array_keys( $info['RateLimits'] ); |
742 | |
743 | if ( isset( $info['AvailableRights'] ) ) { |
744 | $rights = array_diff( $rights, $info['AvailableRights'] ); |
745 | } |
746 | |
747 | $this->globals['wgImplicitRights'] = array_merge( |
748 | $this->globals['wgImplicitRights'] ?? [], |
749 | $rights |
750 | ); |
751 | } |
752 | } |
753 | |
754 | /** |
755 | * @param string $dir |
756 | * @param array $info |
757 | */ |
758 | protected function extractSkinImportPaths( $dir, array $info ) { |
759 | if ( isset( $info['SkinLessImportPaths'] ) ) { |
760 | foreach ( $info['SkinLessImportPaths'] as $skin => $subpath ) { |
761 | $this->attributes['SkinLessImportPaths'][$skin] = "$dir/$subpath"; |
762 | } |
763 | } |
764 | } |
765 | |
766 | /** |
767 | * @param string $path |
768 | * @param array $info |
769 | * @return string Name of thing |
770 | * @throws Exception |
771 | */ |
772 | protected function extractCredits( $path, array $info ) { |
773 | $credits = [ |
774 | 'path' => $path, |
775 | 'type' => 'other', |
776 | ]; |
777 | foreach ( self::CREDIT_ATTRIBS as $attr ) { |
778 | if ( isset( $info[$attr] ) ) { |
779 | $credits[$attr] = $info[$attr]; |
780 | } |
781 | } |
782 | |
783 | $name = $credits['name']; |
784 | |
785 | // If someone is loading the same thing twice, throw |
786 | // a nice error (T121493) |
787 | if ( isset( $this->credits[$name] ) ) { |
788 | $firstPath = $this->credits[$name]['path']; |
789 | $secondPath = $credits['path']; |
790 | throw new InvalidArgumentException( |
791 | "It was attempted to load $name twice, from $firstPath and $secondPath." |
792 | ); |
793 | } |
794 | |
795 | $this->credits[$name] = $credits; |
796 | |
797 | return $name; |
798 | } |
799 | |
800 | protected function extractForeignResourcesDir( array $info, string $name, string $dir ): void { |
801 | if ( array_key_exists( 'ForeignResourcesDir', $info ) ) { |
802 | if ( !is_string( $info['ForeignResourcesDir'] ) ) { |
803 | throw new InvalidArgumentException( "Incorrect ForeignResourcesDir type, must be a string (in $name)" ); |
804 | } |
805 | $this->attributes['ForeignResourcesDir'][$name] = "{$dir}/{$info['ForeignResourcesDir']}"; |
806 | } |
807 | } |
808 | |
809 | /** |
810 | * Set configuration settings for manifest_version == 1 |
811 | * @todo In the future, this should be done via Config interfaces |
812 | * |
813 | * @param array $info |
814 | */ |
815 | protected function extractConfig1( array $info ) { |
816 | if ( isset( $info['config'] ) ) { |
817 | if ( isset( $info['config']['_prefix'] ) ) { |
818 | $prefix = $info['config']['_prefix']; |
819 | unset( $info['config']['_prefix'] ); |
820 | } else { |
821 | $prefix = 'wg'; |
822 | } |
823 | foreach ( $info['config'] as $key => $val ) { |
824 | if ( $key[0] !== '@' ) { |
825 | $this->addConfigGlobal( "$prefix$key", $val, $info['name'] ); |
826 | } |
827 | } |
828 | } |
829 | } |
830 | |
831 | /** |
832 | * Applies a base path to the given string or string array. |
833 | * |
834 | * @param string[] $value |
835 | * @param string $dir |
836 | * |
837 | * @return string[] |
838 | */ |
839 | private function applyPath( array $value, string $dir ): array { |
840 | $result = []; |
841 | |
842 | foreach ( $value as $k => $v ) { |
843 | $result[$k] = $dir . '/' . $v; |
844 | } |
845 | return $result; |
846 | } |
847 | |
848 | /** |
849 | * Set configuration settings for manifest_version == 2 |
850 | * @todo In the future, this should be done via Config interfaces |
851 | * |
852 | * @param array $info |
853 | * @param string $dir |
854 | */ |
855 | protected function extractConfig2( array $info, $dir ) { |
856 | $prefix = $info['config_prefix'] ?? 'wg'; |
857 | if ( isset( $info['config'] ) ) { |
858 | foreach ( $info['config'] as $key => $data ) { |
859 | if ( !array_key_exists( 'value', $data ) ) { |
860 | throw new UnexpectedValueException( "Missing value for config $key" ); |
861 | } |
862 | |
863 | $value = $data['value']; |
864 | if ( isset( $data['path'] ) && $data['path'] ) { |
865 | if ( is_array( $value ) ) { |
866 | $value = $this->applyPath( $value, $dir ); |
867 | } else { |
868 | $value = "$dir/$value"; |
869 | } |
870 | } |
871 | if ( isset( $data['merge_strategy'] ) ) { |
872 | $value[ExtensionRegistry::MERGE_STRATEGY] = $data['merge_strategy']; |
873 | } |
874 | $this->addConfigGlobal( "$prefix$key", $value, $info['name'] ); |
875 | $data['providedby'] = $info['name']; |
876 | if ( isset( $info['ConfigRegistry'][0] ) ) { |
877 | $data['configregistry'] = array_keys( $info['ConfigRegistry'] )[0]; |
878 | } |
879 | } |
880 | } |
881 | } |
882 | |
883 | /** |
884 | * Helper function to set a value to a specific global config variable if it isn't set already. |
885 | * |
886 | * @param string $key The config key with the prefix and anything |
887 | * @param mixed $value The value of the config |
888 | * @param string $extName Name of the extension |
889 | */ |
890 | private function addConfigGlobal( $key, $value, $extName ) { |
891 | if ( array_key_exists( $key, $this->globals ) ) { |
892 | throw new RuntimeException( |
893 | "The configuration setting '$key' was already set by MediaWiki core or" |
894 | . " another extension, and cannot be set again by $extName." ); |
895 | } |
896 | if ( isset( $value[ExtensionRegistry::MERGE_STRATEGY] ) && |
897 | $value[ExtensionRegistry::MERGE_STRATEGY] === 'array_merge_recursive' ) { |
898 | wfDeprecatedMsg( |
899 | "Using the array_merge_recursive merge strategy in extension.json and skin.json" . |
900 | " was deprecated in MediaWiki 1.42", |
901 | "1.42" |
902 | ); |
903 | } |
904 | $this->globals[$key] = $value; |
905 | } |
906 | |
907 | protected function extractPathBasedGlobal( $global, $dir, $paths ) { |
908 | foreach ( $paths as $path ) { |
909 | $this->globals[$global][] = "$dir/$path"; |
910 | } |
911 | } |
912 | |
913 | /** |
914 | * Stores $value to $array; using array_merge_recursive() if $array already contains $name |
915 | * |
916 | * @param string $path |
917 | * @param string $name |
918 | * @param array $value |
919 | * @param array &$array |
920 | * @throws InvalidArgumentException |
921 | */ |
922 | protected function storeToArrayRecursive( $path, $name, $value, &$array ) { |
923 | if ( !is_array( $value ) ) { |
924 | throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" ); |
925 | } |
926 | if ( isset( $array[$name] ) ) { |
927 | $array[$name] = array_merge_recursive( $array[$name], $value ); |
928 | } else { |
929 | $array[$name] = $value; |
930 | } |
931 | } |
932 | |
933 | /** |
934 | * Stores $value to $array; using array_merge() if $array already contains $name |
935 | * |
936 | * @param string $path |
937 | * @param string $name |
938 | * @param array $value |
939 | * @param array &$array |
940 | * @throws InvalidArgumentException |
941 | */ |
942 | protected function storeToArray( $path, $name, $value, &$array ) { |
943 | if ( !is_array( $value ) ) { |
944 | throw new InvalidArgumentException( "The value for '$name' should be an array (from $path)" ); |
945 | } |
946 | if ( isset( $array[$name] ) ) { |
947 | $array[$name] = array_merge( $array[$name], $value ); |
948 | } else { |
949 | $array[$name] = $value; |
950 | } |
951 | } |
952 | |
953 | /** |
954 | * Returns the extracted autoload info. |
955 | * The autoload info is returned as an associative array with three keys: |
956 | * - files: a list of files to load, for use with Autoloader::loadFile() |
957 | * - classes: a map of class names to files, for use with Autoloader::registerClass() |
958 | * - namespaces: a map of namespace names to directories, for use |
959 | * with Autoloader::registerNamespace() |
960 | * |
961 | * @since 1.39 |
962 | * |
963 | * @param bool $includeDev |
964 | * |
965 | * @return array[] The autoload info. |
966 | */ |
967 | public function getExtractedAutoloadInfo( bool $includeDev = false ): array { |
968 | $autoload = $this->autoload; |
969 | |
970 | if ( $includeDev ) { |
971 | $autoload['classes'] += $this->autoloadDev['classes']; |
972 | $autoload['namespaces'] += $this->autoloadDev['namespaces']; |
973 | |
974 | // NOTE: This is here for completeness. Per MW 1.39, |
975 | // $this->autoloadDev['files'] is always empty. |
976 | // So avoid the performance hit of array_merge(). |
977 | if ( !empty( $this->autoloadDev['files'] ) ) { |
978 | // NOTE: Don't use += with numeric keys! |
979 | // Could use PHPUtils::pushArray. |
980 | $autoload['files'] = array_merge( |
981 | $autoload['files'], |
982 | $this->autoloadDev['files'] |
983 | ); |
984 | } |
985 | } |
986 | |
987 | return $autoload; |
988 | } |
989 | |
990 | /** |
991 | * @param array $info |
992 | * @param string $dir |
993 | */ |
994 | private function extractAutoload( array $info, string $dir ) { |
995 | if ( isset( $info['load_composer_autoloader'] ) && $info['load_composer_autoloader'] === true ) { |
996 | $file = "$dir/vendor/autoload.php"; |
997 | if ( file_exists( $file ) ) { |
998 | $this->autoload['files'][] = $file; |
999 | } |
1000 | } |
1001 | |
1002 | if ( isset( $info['AutoloadClasses'] ) ) { |
1003 | $paths = $this->applyPath( $info['AutoloadClasses'], $dir ); |
1004 | $this->autoload['classes'] += $paths; |
1005 | } |
1006 | |
1007 | if ( isset( $info['AutoloadNamespaces'] ) ) { |
1008 | $paths = $this->applyPath( $info['AutoloadNamespaces'], $dir ); |
1009 | $this->autoload['namespaces'] += $paths; |
1010 | } |
1011 | |
1012 | if ( isset( $info['TestAutoloadClasses'] ) ) { |
1013 | $paths = $this->applyPath( $info['TestAutoloadClasses'], $dir ); |
1014 | $this->autoloadDev['classes'] += $paths; |
1015 | } |
1016 | |
1017 | if ( isset( $info['TestAutoloadNamespaces'] ) ) { |
1018 | $paths = $this->applyPath( $info['TestAutoloadNamespaces'], $dir ); |
1019 | $this->autoloadDev['namespaces'] += $paths; |
1020 | } |
1021 | } |
1022 | } |