3 namespace MediaWiki\Tests\Registration
;
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
;
17 * @covers \MediaWiki\Registration\ExtensionRegistry
19 class ExtensionRegistrationTest
extends MediaWikiIntegrationTestCase
{
22 private $autoloaderState;
24 /** @var ?ExtensionRegistry */
25 private $originalExtensionRegistry = null;
27 protected function setUp(): void
{
28 // phpcs:disable MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgHooks
33 $this->autoloaderState
= AutoLoader
::getState();
35 // Make sure to restore globals
36 $this->stashMwGlobals( [
39 'wgNamespaceProtection',
42 'wgAuthManagerAutoConfig',
46 // For the purpose of this test, make $wgHooks behave like a real global config array.
50 protected function tearDown(): void
{
51 AutoLoader
::restoreState( $this->autoloaderState
);
53 if ( $this->originalExtensionRegistry
) {
54 $this->setExtensionRegistry( $this->originalExtensionRegistry
);
60 public function testExportNamespaces() {
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() {
103 public static function onBooEvent() {
107 public function testExportHooks() {
110 'AnEvent' => self
::class . '::onAnEvent',
111 'BooEvent' => 'main',
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() {
134 'events' => [ 'AnEvent', 'BooEvent' ],
135 'factory' => [ self
::class, 'newSubscriber' ]
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,
167 public function testExportAutoload() {
168 global $wgAutoloadClasses;
169 $oldAutoloadClasses = $wgAutoloadClasses;
172 'AutoloadClasses' => [
173 'TestAutoloaderClass' =>
174 __DIR__
. '/../../data/autoloader/TestAutoloadedClass.php',
176 'AutoloadNamespaces' => [
177 'Dummy\Test\Namespace\\' =>
178 __DIR__
. '/../../data/autoloader/psr4/',
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(
231 $this->createMock( ExtensionRegistry
::class ),
232 new ArrayConfigBuilder(),
233 $this->createMock( PhpIniSink
::class ),
240 public static function callbackForTest( array $ext, SettingsBuilder
$settings ) {
241 $settings->overrideConfigValue( 'RunCallbacksTest', 'foo' );
242 self
::assertSame( 'CallbackTest', $ext['name'] );
245 public function testRunCallbacks() {
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).
271 public static function provideExportConfigToGlobals() {
273 'Simple non-array values',
275 'mwtestFooBarConfig' => true,
276 'mwtestFooBarConfig2' => 'string',
279 'config_prefix' => 'mwtest',
281 'FooBarDefault' => [ 'value' => 1234 ],
282 'FooBarConfig' => [ 'value' => false ],
286 'mwtestFooBarConfig' => true,
287 'mwtestFooBarConfig2' => 'string',
288 'mwtestFooBarDefault' => 1234,
293 'No global already set, simple assoc array',
296 'config_prefix' => 'mwtest',
298 'DefaultOptions' => [
306 'mwtestDefaultOptions' => [
313 'No global already set, simple assoc array, manifest version 1',
316 'manifest_version' => 1,
318 '_prefix' => 'mwtest',
332 'Global already set, simple assoc array, manifest version 1',
340 'manifest_version' => 1,
342 '_prefix' => 'mwtest',
359 'Global already set, simple list array',
361 'mwtestList' => [ 'x', 'y', 'z' ],
364 'manifest_version' => 1,
366 '_prefix' => 'mwtest',
367 'List' => [ 'a', 'b' ]
371 'mwtestList' => [ 'a', 'b', 'x', 'y', 'z' ],
376 'New variable, explicit merge strategy',
378 'wgNamespacesFoo' => [
390 'merge_strategy' => 'array_plus',
395 'wgNamespacesFoo' => [
404 'New variable, explicit merge strategy, manifest version 1',
406 'wgNamespacesFoo' => [
412 'manifest_version' => 1,
417 ExtensionRegistry
::MERGE_STRATEGY
=> 'array_plus',
422 'wgNamespacesFoo' => [
431 'False local setting should not be overridden by default (T100767)',
433 'wgT100767' => false,
437 'T100767' => [ 'value' => true ],
441 'wgT100767' => false,
446 'test array_replace_recursive',
448 'mwtestJsonConfigs' => [
449 'JsonZeroConfig' => [
460 'config_prefix' => 'mwtest',
464 'JsonZeroConfig' => [
468 'merge_strategy' => 'array_replace_recursive',
473 'mwtestJsonConfigs' => [
474 'JsonZeroConfig' => [
487 'Default doesn\'t override null',
489 'wgNullGlobal' => null,
493 'NullGlobal' => [ 'value' => 'not-null' ]
497 'wgNullGlobal' => null
502 'provide_default passive case',
510 'merge_strategy' => 'provide_default'
520 'provide_default active case',
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.
542 public static function provideExportAttributesToGlobals() {
544 'AvailableRights appends to default value, per config schema',
546 'wgAvailableRights' => [
551 [ 'AvailableRights' => [ 'ccc', ] ],
553 // NOTE: This is backwards! Fortunately, the order in AvailableRights
554 // is not significant.
555 'wgAvailableRights' => [
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' ],
587 'Global already set, $wgGroupPermissions',
589 'wgGroupPermissions' => [
594 'somethingtwo' => true,
599 'GroupPermissions' => [
605 'somethingtwo' => false,
606 'nonduplicated' => true,
611 'wgGroupPermissions' => [
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,
625 'nonduplicated' => true,
633 * @param array $manifest
637 private function makeManifestFile( array $manifest ): string {
640 'manifest_version' => 2,
646 'autoloaderPaths' => []
649 $file = $this->getNewTempFile();
650 file_put_contents( $file, json_encode( $manifest ) );
654 public function testExportAutoloaderWithPsr4Namespaces() {
655 $dir = __DIR__
. '/../../data/registration';
656 $registry = new ExtensionRegistry();
657 $data = $registry->readFromQueue( [
658 "{$dir}/autoload_namespaces.json" => 1
661 $access = TestingAccessWrapper
::newFromObject( $registry );
662 $access->exportExtractedData( $data );
665 class_exists( 'Test\\MediaWiki\\AutoLoader\\TestFooBar' ),
666 "Registry initializes Autoloader from AutoloadNamespaces"