Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / registration / ExtensionRegistrationTest.php
blobc19225d5c06daa023d08e41fbecf7ad15b57b750
1 <?php
3 namespace MediaWiki\Tests\Registration;
5 use AutoLoader;
6 use Generator;
7 use MediaWiki\DomainEvent\DomainEventSource;
8 use MediaWiki\Registration\ExtensionRegistry;
9 use MediaWiki\Settings\Config\ArrayConfigBuilder;
10 use MediaWiki\Settings\Config\PhpIniSink;
11 use MediaWiki\Settings\SettingsBuilder;
12 use MediaWikiIntegrationTestCase;
13 use Wikimedia\ObjectCache\HashBagOStuff;
14 use Wikimedia\TestingAccessWrapper;
16 /**
17 * @covers \MediaWiki\Registration\ExtensionRegistry
19 class ExtensionRegistrationTest extends MediaWikiIntegrationTestCase {
21 /** @var array */
22 private $autoloaderState;
24 /** @var ?ExtensionRegistry */
25 private $originalExtensionRegistry = null;
27 protected function setUp(): void {
28 // phpcs:disable MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgHooks
29 global $wgHooks;
31 parent::setUp();
33 $this->autoloaderState = AutoLoader::getState();
35 // Make sure to restore globals
36 $this->stashMwGlobals( [
37 'wgHooks',
38 'wgAutoloadClasses',
39 'wgNamespaceProtection',
40 'wgNamespaceModels',
41 'wgAvailableRights',
42 'wgAuthManagerAutoConfig',
43 'wgGroupPermissions',
44 ] );
46 // For the purpose of this test, make $wgHooks behave like a real global config array.
47 $wgHooks = [];
50 protected function tearDown(): void {
51 AutoLoader::restoreState( $this->autoloaderState );
53 if ( $this->originalExtensionRegistry ) {
54 $this->setExtensionRegistry( $this->originalExtensionRegistry );
57 parent::tearDown();
60 public function testExportNamespaces() {
61 $manifest = [
62 'namespaces' => [
64 'id' => 1300,
65 'name' => 'ExtensionRegistrationTest',
66 'constant' => 'NS_EXTENSION_REGISTRATION_TEST',
67 'defaultcontentmodel' => 'Foo',
68 'protection' => [ 'sysop' ],
73 $file = $this->makeManifestFile( $manifest );
75 $registry = new ExtensionRegistry();
76 $registry->queue( $file );
77 $registry->loadFromQueue();
79 $this->assertTrue( defined( 'NS_EXTENSION_REGISTRATION_TEST' ) );
80 $this->assertSame( 1300, constant( 'NS_EXTENSION_REGISTRATION_TEST' ) );
82 $expectedNamespaceNames = [ 1300 => 'ExtensionRegistrationTest' ];
83 $this->assertSame( $expectedNamespaceNames, $registry->getAttribute( 'ExtensionNamespaces' ) );
85 $this->assertArrayHasKey( 1300, $GLOBALS['wgNamespaceProtection'] );
86 $this->assertArrayHasKey( 1300, $GLOBALS['wgNamespaceContentModels'] );
89 private function setExtensionRegistry( ExtensionRegistry $registry ) {
90 $class = new \ReflectionClass( ExtensionRegistry::class );
92 if ( !$this->originalExtensionRegistry ) {
93 $this->originalExtensionRegistry = $class->getStaticPropertyValue( 'instance' );
96 $class->setStaticPropertyValue( 'instance', $registry );
99 public static function onAnEvent() {
100 // no-op
103 public static function onBooEvent() {
104 // no-op
107 public function testExportHooks() {
108 $manifest = [
109 'Hooks' => [
110 'AnEvent' => self::class . '::onAnEvent',
111 'BooEvent' => 'main',
113 'HookHandlers' => [
114 'main' => [ 'class' => self::class ]
118 $file = $this->makeManifestFile( $manifest );
120 $registry = new ExtensionRegistry();
121 $this->setExtensionRegistry( $registry );
123 $registry->queue( $file );
124 $registry->loadFromQueue();
126 $this->resetServices();
127 $hookContainer = $this->getServiceContainer()->getHookContainer();
128 $this->assertTrue( $hookContainer->isRegistered( 'AnEvent' ), 'AnEvent' );
129 $this->assertTrue( $hookContainer->isRegistered( 'BooEvent' ), 'BooEvent' );
132 public function testRegisterDomainEventListeners() {
133 $subscriber = [
134 'events' => [ 'AnEvent', 'BooEvent' ],
135 'factory' => [ self::class, 'newSubscriber' ]
138 $manifest = [
139 'DomainEventSubscribers' => [ $subscriber ]
142 $file = $this->makeManifestFile( $manifest );
144 $registry = new ExtensionRegistry();
145 $this->setExtensionRegistry( $registry );
147 $registry->queue( $file );
148 $registry->loadFromQueue();
150 $actualSubscribers = [];
151 $mockSource = $this->createMock( DomainEventSource::class );
152 $mockSource->method( 'registerSubscriber' )->willReturnCallback(
153 static function ( $subscriber ) use ( &$actualSubscribers ) {
154 $actualSubscribers[] = $subscriber;
157 $registry->registerListeners( $mockSource );
159 $expectedSubscribers = [ $subscriber + [ 'extensionPath' => $file ] ];
161 $this->assertArrayEquals(
162 $expectedSubscribers,
163 $actualSubscribers
167 public function testExportAutoload() {
168 global $wgAutoloadClasses;
169 $oldAutoloadClasses = $wgAutoloadClasses;
171 $manifest = [
172 'AutoloadClasses' => [
173 'TestAutoloaderClass' =>
174 __DIR__ . '/../../data/autoloader/TestAutoloadedClass.php',
176 'AutoloadNamespaces' => [
177 'Dummy\Test\Namespace\\' =>
178 __DIR__ . '/../../data/autoloader/psr4/',
180 'HookHandler' => [
181 'main' => [ 'class' => 'Whatever' ]
185 $file = $this->makeManifestFile( $manifest );
187 $registry = new ExtensionRegistry();
188 $registry->setCache( new HashBagOStuff() );
190 $registry->queue( $file );
191 $registry->loadFromQueue();
193 $this->assertArrayHasKey( 'TestAutoloaderClass', AutoLoader::getClassFiles() );
194 $this->assertArrayHasKey( 'Dummy\Test\Namespace\\', AutoLoader::getNamespaceDirectories() );
196 // Now, reset and do it again, but with the cached extension info.
197 // This is needed because autoloader registration is currently handled
198 // differently when loading from the cache (T240535).
199 AutoLoader::restoreState( $this->autoloaderState );
200 $wgAutoloadClasses = $oldAutoloadClasses;
202 $registry->queue( $file );
203 $registry->loadFromQueue();
205 $this->assertArrayHasKey( 'TestAutoloaderClass', AutoLoader::getClassFiles() );
206 $this->assertArrayHasKey( 'Dummy\Test\Namespace\\', AutoLoader::getNamespaceDirectories() );
210 * @dataProvider provideExportConfigToGlobals
211 * @dataProvider provideExportAttributesToGlobals
213 public function testExportGlobals( $desc, $before, $manifest, $expected ) {
214 $this->setMwGlobals( $before );
216 $file = $this->makeManifestFile( $manifest );
218 $registry = new ExtensionRegistry();
219 $registry->queue( $file );
220 $registry->loadFromQueue();
222 foreach ( $expected as $name => $expectedValue ) {
223 $this->assertArrayHasKey( $name, $GLOBALS, $desc );
224 $this->assertEquals( $expectedValue, $GLOBALS[$name], $desc );
228 private function newSettingsBuilder(): SettingsBuilder {
229 $settings = new SettingsBuilder(
230 __DIR__,
231 $this->createMock( ExtensionRegistry::class ),
232 new ArrayConfigBuilder(),
233 $this->createMock( PhpIniSink::class ),
234 null
237 return $settings;
240 public static function callbackForTest( array $ext, SettingsBuilder $settings ) {
241 $settings->overrideConfigValue( 'RunCallbacksTest', 'foo' );
242 self::assertSame( 'CallbackTest', $ext['name'] );
245 public function testRunCallbacks() {
246 $manifest = [
247 'name' => 'CallbackTest',
248 'callback' => [ __CLASS__, 'callbackForTest' ],
251 $file = $this->makeManifestFile( $manifest );
253 $settings = $this->newSettingsBuilder();
255 $registry = new ExtensionRegistry();
256 $registry->setSettingsBuilder( $settings );
258 $settings->enterRegistrationStage();
259 $registry->queue( $file );
260 $registry->loadFromQueue();
262 $this->assertSame( 'foo', $settings->getConfig()->get( 'RunCallbacksTest' ) );
266 * Provides defaults coming from extension, global values from custom settings.
267 * The global value should be merged on top of the default from the extension (backwards merge).
269 * @return Generator
271 public static function provideExportConfigToGlobals() {
272 yield [
273 'Simple non-array values',
275 'mwtestFooBarConfig' => true,
276 'mwtestFooBarConfig2' => 'string',
279 'config_prefix' => 'mwtest',
280 'config' => [
281 'FooBarDefault' => [ 'value' => 1234 ],
282 'FooBarConfig' => [ 'value' => false ],
286 'mwtestFooBarConfig' => true,
287 'mwtestFooBarConfig2' => 'string',
288 'mwtestFooBarDefault' => 1234,
292 yield [
293 'No global already set, simple assoc array',
296 'config_prefix' => 'mwtest',
297 'config' => [
298 'DefaultOptions' => [
299 'value' => [
300 'foobar' => true,
306 'mwtestDefaultOptions' => [
307 'foobar' => true,
312 yield [
313 'No global already set, simple assoc array, manifest version 1',
316 'manifest_version' => 1,
317 'config' => [
318 '_prefix' => 'mwtest',
319 'SomeMap' => [
320 'foobar' => true,
325 'mwtestSomeMap' => [
326 'foobar' => true,
331 yield [
332 'Global already set, simple assoc array, manifest version 1',
334 'mwtestSomeMap' => [
335 'foobar' => true,
336 'foo' => 'string'
340 'manifest_version' => 1,
341 'config' => [
342 '_prefix' => 'mwtest',
343 'SomeMap' => [
344 'barbaz' => 12345,
345 'foobar' => false,
350 'mwtestSomeMap' => [
351 'barbaz' => 12345,
352 'foo' => 'string',
353 'foobar' => true,
358 yield [
359 'Global already set, simple list array',
361 'mwtestList' => [ 'x', 'y', 'z' ],
364 'manifest_version' => 1,
365 'config' => [
366 '_prefix' => 'mwtest',
367 'List' => [ 'a', 'b' ]
371 'mwtestList' => [ 'a', 'b', 'x', 'y', 'z' ],
375 yield [
376 'New variable, explicit merge strategy',
378 'wgNamespacesFoo' => [
379 100 => true,
380 102 => false
384 'config' => [
385 'NamespacesFoo' => [
386 'value' => [
387 100 => false,
388 500 => true,
390 'merge_strategy' => 'array_plus',
395 'wgNamespacesFoo' => [
396 100 => true,
397 102 => false,
398 500 => true,
403 yield [
404 'New variable, explicit merge strategy, manifest version 1',
406 'wgNamespacesFoo' => [
407 100 => true,
408 102 => false
412 'manifest_version' => 1,
413 'config' => [
414 'NamespacesFoo' => [
415 100 => false,
416 500 => true,
417 ExtensionRegistry::MERGE_STRATEGY => 'array_plus',
422 'wgNamespacesFoo' => [
423 100 => true,
424 102 => false,
425 500 => true,
430 yield [
431 'False local setting should not be overridden by default (T100767)',
433 'wgT100767' => false,
436 'config' => [
437 'T100767' => [ 'value' => true ],
441 'wgT100767' => false,
445 yield [
446 'test array_replace_recursive',
448 'mwtestJsonConfigs' => [
449 'JsonZeroConfig' => [
450 'namespace' => 480,
451 'nsName' => 'Zero',
452 'isLocal' => false,
453 'remote' => [
454 'username' => 'foo',
460 'config_prefix' => 'mwtest',
461 'config' => [
462 'JsonConfigs' => [
463 'value' => [
464 'JsonZeroConfig' => [
465 'isLocal' => true,
468 'merge_strategy' => 'array_replace_recursive',
473 'mwtestJsonConfigs' => [
474 'JsonZeroConfig' => [
475 'namespace' => 480,
476 'nsName' => 'Zero',
477 'isLocal' => false,
478 'remote' => [
479 'username' => 'foo',
486 yield [
487 'Default doesn\'t override null',
489 'wgNullGlobal' => null,
492 'config' => [
493 'NullGlobal' => [ 'value' => 'not-null' ]
497 'wgNullGlobal' => null
501 yield [
502 'provide_default passive case',
504 'wgFlatArray' => [],
507 'config' => [
508 'FlatArray' => [
509 'value' => [ 1 ],
510 'merge_strategy' => 'provide_default'
515 'wgFlatArray' => []
519 yield [
520 'provide_default active case',
523 'config' => [
524 'FlatArray' => [
525 'value' => [ 1 ],
526 'merge_strategy' => 'provide_default'
531 'wgFlatArray' => [ 1 ]
537 * Provide global values as default coming from core, new value from extension attribute.
538 * The value coming from the extension should be merged on top of the global.
540 * @return Generator
542 public static function provideExportAttributesToGlobals() {
543 yield [
544 'AvailableRights appends to default value, per config schema',
546 'wgAvailableRights' => [
547 'aaa',
548 'bbb'
551 [ 'AvailableRights' => [ 'ccc', ] ],
553 // NOTE: This is backwards! Fortunately, the order in AvailableRights
554 // is not significant.
555 'wgAvailableRights' => [
556 'ccc',
557 'aaa',
558 'bbb',
563 yield [
564 'AuthManagerAutoConfig appends to default value, per top level key',
566 'wgAuthManagerAutoConfig' => [
567 'preauth' => [ 'default' => 'DefaultPreAuth' ],
568 'primaryauth' => [ 'default' => 'DefaultPrimaryAuth' ],
569 'secondaryauth' => [ 'default' => 'DefaultSecondaryAuth' ],
573 'AuthManagerAutoConfig' => [
574 'primaryauth' => [ 'my' => 'MyPrimaryAuth' ],
578 'wgAuthManagerAutoConfig' => [
579 'preauth' => [ 'default' => 'DefaultPreAuth' ],
580 'primaryauth' => [ 'default' => 'DefaultPrimaryAuth', 'my' => 'MyPrimaryAuth' ],
581 'secondaryauth' => [ 'default' => 'DefaultSecondaryAuth' ],
586 yield [
587 'Global already set, $wgGroupPermissions',
589 'wgGroupPermissions' => [
590 'sysop' => [
591 'something' => true,
593 'user' => [
594 'somethingtwo' => true,
599 'GroupPermissions' => [
600 'customgroup' => [
601 'right' => true,
603 'user' => [
604 'right' => true,
605 'somethingtwo' => false,
606 'nonduplicated' => true,
611 'wgGroupPermissions' => [
612 'customgroup' => [
613 'right' => true,
615 'sysop' => [
616 'something' => true,
618 'user' => [
619 // NOTE: somethingtwo should be false here, since the value from
620 // the extension should override the core default!
621 // See e.g. https://www.mediawiki.org/wiki/Topic:W2ttbedo3apzno4w
622 // and https://phabricator.wikimedia.org/T98347#2589540.
623 'somethingtwo' => true,
624 'right' => true,
625 'nonduplicated' => true,
633 * @param array $manifest
635 * @return string
637 private function makeManifestFile( array $manifest ): string {
638 $manifest += [
639 'name' => 'Test',
640 'manifest_version' => 2,
641 'config' => [],
642 'callbacks' => [],
643 'defines' => [],
644 'credits' => [],
645 'attributes' => [],
646 'autoloaderPaths' => []
649 $file = $this->getNewTempFile();
650 file_put_contents( $file, json_encode( $manifest ) );
651 return $file;
654 public function testExportAutoloaderWithPsr4Namespaces() {
655 $dir = __DIR__ . '/../../data/registration';
656 $registry = new ExtensionRegistry();
657 $data = $registry->readFromQueue( [
658 "{$dir}/autoload_namespaces.json" => 1
659 ] );
661 $access = TestingAccessWrapper::newFromObject( $registry );
662 $access->exportExtractedData( $data );
664 $this->assertTrue(
665 class_exists( 'Test\\MediaWiki\\AutoLoader\\TestFooBar' ),
666 "Registry initializes Autoloader from AutoloadNamespaces"