Merge "Revert "Revert "Fix the web updater"""
[lhc/web/wiklou.git] / tests / phpunit / includes / registration / ExtensionProcessorTest.php
1 <?php
2
3 use Wikimedia\TestingAccessWrapper;
4
5 class ExtensionProcessorTest extends MediaWikiTestCase {
6
7 private $dir, $dirname;
8
9 public function setUp() {
10 parent::setUp();
11 $this->dir = __DIR__ . '/FooBar/extension.json';
12 $this->dirname = dirname( $this->dir );
13 }
14
15 /**
16 * 'name' is absolutely required
17 *
18 * @var array
19 */
20 public static $default = [
21 'name' => 'FooBar',
22 ];
23
24 /**
25 * @covers ExtensionProcessor::extractInfo
26 */
27 public function testExtractInfo() {
28 // Test that attributes that begin with @ are ignored
29 $processor = new ExtensionProcessor();
30 $processor->extractInfo( $this->dir, self::$default + [
31 '@metadata' => [ 'foobarbaz' ],
32 'AnAttribute' => [ 'omg' ],
33 'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
34 ], 1 );
35
36 $extracted = $processor->getExtractedInfo();
37 $attributes = $extracted['attributes'];
38 $this->assertArrayHasKey( 'AnAttribute', $attributes );
39 $this->assertArrayNotHasKey( '@metadata', $attributes );
40 $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
41 }
42
43 /**
44 * @covers ExtensionProcessor::extractInfo
45 */
46 public function testExtractInfo_namespaces() {
47 // Test that namespace IDs can be overwritten
48 if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
49 define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
50 }
51
52 $processor = new ExtensionProcessor();
53 $processor->extractInfo( $this->dir, self::$default + [
54 'namespaces' => [
55 [
56 'id' => 332200,
57 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
58 'name' => 'Test_A',
59 'content' => 'TestModel'
60 ],
61 [ // Test_X will use ID 123456 not 334400
62 'id' => 334400,
63 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
64 'name' => 'Test_X',
65 'content' => 'TestModel'
66 ],
67 ]
68 ], 1 );
69
70 $extracted = $processor->getExtractedInfo();
71
72 $this->assertArrayHasKey(
73 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
74 $extracted['defines']
75 );
76 $this->assertArrayNotHasKey(
77 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
78 $extracted['defines']
79 );
80
81 $this->assertSame(
82 $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'],
83 332200
84 );
85
86 $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
87 $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
88 $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
89 $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
90
91 $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
92 $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
93 }
94
95 public static function provideRegisterHooks() {
96 $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
97 // Format:
98 // Current $wgHooks
99 // Content in extension.json
100 // Expected value of $wgHooks
101 return [
102 // No hooks
103 [
104 [],
105 self::$default,
106 $merge,
107 ],
108 // No current hooks, adding one for "FooBaz" in string format
109 [
110 [],
111 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
112 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
113 ],
114 // Hook for "FooBaz", adding another one
115 [
116 [ 'FooBaz' => [ 'PriorCallback' ] ],
117 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
118 [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
119 ],
120 // No current hooks, adding one for "FooBaz" in verbose array format
121 [
122 [],
123 [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
124 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
125 ],
126 // Hook for "BarBaz", adding one for "FooBaz"
127 [
128 [ 'BarBaz' => [ 'BarBazCallback' ] ],
129 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
130 [
131 'BarBaz' => [ 'BarBazCallback' ],
132 'FooBaz' => [ 'FooBazCallback' ],
133 ] + $merge,
134 ],
135 // Callbacks for FooBaz wrapped in an array
136 [
137 [],
138 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
139 [
140 'FooBaz' => [ 'Callback1' ],
141 ] + $merge,
142 ],
143 // Multiple callbacks for FooBaz hook
144 [
145 [],
146 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
147 [
148 'FooBaz' => [ 'Callback1', 'Callback2' ],
149 ] + $merge,
150 ],
151 ];
152 }
153
154 /**
155 * @covers ExtensionProcessor::extractHooks
156 * @dataProvider provideRegisterHooks
157 */
158 public function testRegisterHooks( $pre, $info, $expected ) {
159 $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
160 $processor->extractInfo( $this->dir, $info, 1 );
161 $extracted = $processor->getExtractedInfo();
162 $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
163 }
164
165 /**
166 * @covers ExtensionProcessor::extractConfig1
167 */
168 public function testExtractConfig1() {
169 $processor = new ExtensionProcessor;
170 $info = [
171 'config' => [
172 'Bar' => 'somevalue',
173 'Foo' => 10,
174 '@IGNORED' => 'yes',
175 ],
176 ] + self::$default;
177 $info2 = [
178 'config' => [
179 '_prefix' => 'eg',
180 'Bar' => 'somevalue'
181 ],
182 'name' => 'FooBar2',
183 ];
184 $processor->extractInfo( $this->dir, $info, 1 );
185 $processor->extractInfo( $this->dir, $info2, 1 );
186 $extracted = $processor->getExtractedInfo();
187 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
188 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
189 $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
190 // Custom prefix:
191 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
192 }
193
194 /**
195 * @covers ExtensionProcessor::extractConfig2
196 */
197 public function testExtractConfig2() {
198 $processor = new ExtensionProcessor;
199 $info = [
200 'config' => [
201 'Bar' => [ 'value' => 'somevalue' ],
202 'Foo' => [ 'value' => 10 ],
203 'Path' => [ 'value' => 'foo.txt', 'path' => true ],
204 ],
205 ] + self::$default;
206 $info2 = [
207 'config' => [
208 'Bar' => [ 'value' => 'somevalue' ],
209 ],
210 'config_prefix' => 'eg',
211 'name' => 'FooBar2',
212 ];
213 $processor->extractInfo( $this->dir, $info, 2 );
214 $processor->extractInfo( $this->dir, $info2, 2 );
215 $extracted = $processor->getExtractedInfo();
216 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
217 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
218 $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
219 // Custom prefix:
220 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
221 }
222
223 public static function provideExtractExtensionMessagesFiles() {
224 $dir = __DIR__ . '/FooBar/';
225 return [
226 [
227 [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
228 [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
229 ],
230 [
231 [
232 'ExtensionMessagesFiles' => [
233 'FooBarAlias' => 'FooBar.alias.php',
234 'FooBarMagic' => 'FooBar.magic.i18n.php',
235 ],
236 ],
237 [
238 'wgExtensionMessagesFiles' => [
239 'FooBarAlias' => $dir . 'FooBar.alias.php',
240 'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
241 ],
242 ],
243 ],
244 ];
245 }
246
247 /**
248 * @covers ExtensionProcessor::extractExtensionMessagesFiles
249 * @dataProvider provideExtractExtensionMessagesFiles
250 */
251 public function testExtractExtensionMessagesFiles( $input, $expected ) {
252 $processor = new ExtensionProcessor();
253 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
254 $out = $processor->getExtractedInfo();
255 foreach ( $expected as $key => $value ) {
256 $this->assertEquals( $value, $out['globals'][$key] );
257 }
258 }
259
260 public static function provideExtractMessagesDirs() {
261 $dir = __DIR__ . '/FooBar/';
262 return [
263 [
264 [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
265 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
266 ],
267 [
268 [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
269 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
270 ],
271 ];
272 }
273
274 /**
275 * @covers ExtensionProcessor::extractMessagesDirs
276 * @dataProvider provideExtractMessagesDirs
277 */
278 public function testExtractMessagesDirs( $input, $expected ) {
279 $processor = new ExtensionProcessor();
280 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
281 $out = $processor->getExtractedInfo();
282 foreach ( $expected as $key => $value ) {
283 $this->assertEquals( $value, $out['globals'][$key] );
284 }
285 }
286
287 /**
288 * @covers ExtensionProcessor::extractCredits
289 */
290 public function testExtractCredits() {
291 $processor = new ExtensionProcessor();
292 $processor->extractInfo( $this->dir, self::$default, 1 );
293 $this->setExpectedException( 'Exception' );
294 $processor->extractInfo( $this->dir, self::$default, 1 );
295 }
296
297 /**
298 * @covers ExtensionProcessor::extractResourceLoaderModules
299 * @dataProvider provideExtractResourceLoaderModules
300 */
301 public function testExtractResourceLoaderModules( $input, $expected ) {
302 $processor = new ExtensionProcessor();
303 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
304 $out = $processor->getExtractedInfo();
305 foreach ( $expected as $key => $value ) {
306 $this->assertEquals( $value, $out['globals'][$key] );
307 }
308 }
309
310 public static function provideExtractResourceLoaderModules() {
311 $dir = __DIR__ . '/FooBar';
312 return [
313 // Generic module with localBasePath/remoteExtPath specified
314 [
315 // Input
316 [
317 'ResourceModules' => [
318 'test.foo' => [
319 'styles' => 'foobar.js',
320 'localBasePath' => '',
321 'remoteExtPath' => 'FooBar',
322 ],
323 ],
324 ],
325 // Expected
326 [
327 'wgResourceModules' => [
328 'test.foo' => [
329 'styles' => 'foobar.js',
330 'localBasePath' => $dir,
331 'remoteExtPath' => 'FooBar',
332 ],
333 ],
334 ],
335 ],
336 // ResourceFileModulePaths specified:
337 [
338 // Input
339 [
340 'ResourceFileModulePaths' => [
341 'localBasePath' => '',
342 'remoteExtPath' => 'FooBar',
343 ],
344 'ResourceModules' => [
345 // No paths
346 'test.foo' => [
347 'styles' => 'foo.js',
348 ],
349 // Different paths set
350 'test.bar' => [
351 'styles' => 'bar.js',
352 'localBasePath' => 'subdir',
353 'remoteExtPath' => 'FooBar/subdir',
354 ],
355 // Custom class with no paths set
356 'test.class' => [
357 'class' => 'FooBarModule',
358 'extra' => 'argument',
359 ],
360 // Custom class with a localBasePath
361 'test.class.with.path' => [
362 'class' => 'FooBarPathModule',
363 'extra' => 'argument',
364 'localBasePath' => '',
365 ]
366 ],
367 ],
368 // Expected
369 [
370 'wgResourceModules' => [
371 'test.foo' => [
372 'styles' => 'foo.js',
373 'localBasePath' => $dir,
374 'remoteExtPath' => 'FooBar',
375 ],
376 'test.bar' => [
377 'styles' => 'bar.js',
378 'localBasePath' => "$dir/subdir",
379 'remoteExtPath' => 'FooBar/subdir',
380 ],
381 'test.class' => [
382 'class' => 'FooBarModule',
383 'extra' => 'argument',
384 'localBasePath' => $dir,
385 'remoteExtPath' => 'FooBar',
386 ],
387 'test.class.with.path' => [
388 'class' => 'FooBarPathModule',
389 'extra' => 'argument',
390 'localBasePath' => $dir,
391 'remoteExtPath' => 'FooBar',
392 ]
393 ],
394 ],
395 ],
396 // ResourceModuleSkinStyles with file module paths
397 [
398 // Input
399 [
400 'ResourceFileModulePaths' => [
401 'localBasePath' => '',
402 'remoteSkinPath' => 'FooBar',
403 ],
404 'ResourceModuleSkinStyles' => [
405 'foobar' => [
406 'test.foo' => 'foo.css',
407 ]
408 ],
409 ],
410 // Expected
411 [
412 'wgResourceModuleSkinStyles' => [
413 'foobar' => [
414 'test.foo' => 'foo.css',
415 'localBasePath' => $dir,
416 'remoteSkinPath' => 'FooBar',
417 ],
418 ],
419 ],
420 ],
421 // ResourceModuleSkinStyles with file module paths and an override
422 [
423 // Input
424 [
425 'ResourceFileModulePaths' => [
426 'localBasePath' => '',
427 'remoteSkinPath' => 'FooBar',
428 ],
429 'ResourceModuleSkinStyles' => [
430 'foobar' => [
431 'test.foo' => 'foo.css',
432 'remoteSkinPath' => 'BarFoo'
433 ],
434 ],
435 ],
436 // Expected
437 [
438 'wgResourceModuleSkinStyles' => [
439 'foobar' => [
440 'test.foo' => 'foo.css',
441 'localBasePath' => $dir,
442 'remoteSkinPath' => 'BarFoo',
443 ],
444 ],
445 ],
446 ],
447 ];
448 }
449
450 public static function provideSetToGlobal() {
451 return [
452 [
453 [ 'wgAPIModules', 'wgAvailableRights' ],
454 [],
455 [
456 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
457 'AvailableRights' => [ 'foobar', 'unfoobar' ],
458 ],
459 [
460 'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
461 'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
462 ],
463 ],
464 [
465 [ 'wgAPIModules', 'wgAvailableRights' ],
466 [
467 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
468 'wgAvailableRights' => [ 'barbaz' ]
469 ],
470 [
471 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
472 'AvailableRights' => [ 'foobar', 'unfoobar' ],
473 ],
474 [
475 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
476 'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
477 ],
478 ],
479 [
480 [ 'wgGroupPermissions' ],
481 [
482 'wgGroupPermissions' => [
483 'sysop' => [ 'delete' ]
484 ],
485 ],
486 [
487 'GroupPermissions' => [
488 'sysop' => [ 'undelete' ],
489 'user' => [ 'edit' ]
490 ],
491 ],
492 [
493 'wgGroupPermissions' => [
494 'sysop' => [ 'delete', 'undelete' ],
495 'user' => [ 'edit' ]
496 ],
497 ]
498 ]
499 ];
500 }
501
502 /**
503 * Attributes under manifest_version 2
504 *
505 * @covers ExtensionProcessor::extractAttributes
506 * @covers ExtensionProcessor::getExtractedInfo
507 */
508 public function testExtractAttributes() {
509 $processor = new ExtensionProcessor();
510 // Load FooBar extension
511 $processor->extractInfo( $this->dir, [ 'name' => 'FooBar' ], 2 );
512 $processor->extractInfo(
513 $this->dir,
514 [
515 'name' => 'Baz',
516 'attributes' => [
517 // Loaded
518 'FooBar' => [
519 'Plugins' => [
520 'ext.baz.foobar',
521 ],
522 ],
523 // Not loaded
524 'FizzBuzz' => [
525 'MorePlugins' => [
526 'ext.baz.fizzbuzz',
527 ],
528 ],
529 ],
530 ],
531 2
532 );
533
534 $info = $processor->getExtractedInfo();
535 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
536 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
537 $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
538 }
539
540 /**
541 * Attributes under manifest_version 1
542 *
543 * @covers ExtensionProcessor::extractInfo
544 */
545 public function testAttributes1() {
546 $processor = new ExtensionProcessor();
547 $processor->extractInfo(
548 $this->dir,
549 [
550 'name' => 'FooBar',
551 'FooBarPlugins' => [
552 'ext.baz.foobar',
553 ],
554 'FizzBuzzMorePlugins' => [
555 'ext.baz.fizzbuzz',
556 ],
557 ],
558 1
559 );
560
561 $info = $processor->getExtractedInfo();
562 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
563 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
564 $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
565 $this->assertSame( [ 'ext.baz.fizzbuzz' ], $info['attributes']['FizzBuzzMorePlugins'] );
566 }
567
568 public function testGlobalSettingsDocumentedInSchema() {
569 global $IP;
570 $globalSettings = TestingAccessWrapper::newFromClass(
571 ExtensionProcessor::class )->globalSettings;
572
573 $version = ExtensionRegistry::MANIFEST_VERSION;
574 $schema = FormatJson::decode(
575 file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
576 true
577 );
578 $missing = [];
579 foreach ( $globalSettings as $global ) {
580 if ( !isset( $schema['properties'][$global] ) ) {
581 $missing[] = $global;
582 }
583 }
584
585 $this->assertEquals( [], $missing,
586 "The following global settings are not documented in docs/extension.schema.json" );
587 }
588 }
589
590 /**
591 * Allow overriding the default value of $this->globals
592 * so we can test merging
593 */
594 class MockExtensionProcessor extends ExtensionProcessor {
595 public function __construct( $globals = [] ) {
596 $this->globals = $globals + $this->globals;
597 }
598 }