Previously, SettingsBuilder would allow configuration to be loaded and modified until it was "finalized", at which point configuration became read only. This patch introduces an intermediate stage where registration dynamic manipulation of config values can be performed, after all extensions have been loaded and all config schemas are known. Motivation: Extension registration callbacks are typically used to dynamically set config variables, often based on other configuration. This should be done using SettingsBuilder rather than global variables. But previously, we could only be sure that all extensions are known after SettingsBuilder was "finalized", at which point it would be impossible to change config values. Change-Id: I6f8f9f3f7252f0024282d7b005671f28a5b3acc3
663 lines
14 KiB
PHP
663 lines
14 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\Registration;
|
|
|
|
use AutoLoader;
|
|
use ExtensionRegistry;
|
|
use Generator;
|
|
use HashBagOStuff;
|
|
use MediaWiki\Settings\Config\ArrayConfigBuilder;
|
|
use MediaWiki\Settings\Config\PhpIniSink;
|
|
use MediaWiki\Settings\SettingsBuilder;
|
|
use MediaWikiIntegrationTestCase;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @covers ExtensionRegistry
|
|
*/
|
|
class ExtensionRegistrationTest extends MediaWikiIntegrationTestCase {
|
|
|
|
private $autoloaderState;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
|
|
$this->autoloaderState = AutoLoader::getState();
|
|
|
|
// Make sure to restore globals
|
|
$this->stashMwGlobals( [
|
|
'wgAutoloadClasses',
|
|
'wgHooks',
|
|
'wgNamespaceProtection',
|
|
'wgNamespaceModels',
|
|
'wgAvailableRights',
|
|
'wgAuthManagerAutoConfig',
|
|
'wgGroupPermissions',
|
|
] );
|
|
}
|
|
|
|
protected function tearDown(): void {
|
|
AutoLoader::restoreState( $this->autoloaderState );
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testExportNamespaces() {
|
|
$manifest = [
|
|
'namespaces' => [
|
|
[
|
|
'id' => 1300,
|
|
'name' => 'ExtensionRegistrationTest',
|
|
'constant' => 'NS_EXTENSION_REGISTRATION_TEST',
|
|
'defaultcontentmodel' => 'Foo',
|
|
'protection' => [ 'sysop' ],
|
|
]
|
|
]
|
|
];
|
|
|
|
$file = $this->makeManifestFile( $manifest );
|
|
|
|
$registry = new ExtensionRegistry();
|
|
$registry->queue( $file );
|
|
$registry->loadFromQueue();
|
|
|
|
$this->assertTrue( defined( 'NS_EXTENSION_REGISTRATION_TEST' ) );
|
|
$this->assertSame( 1300, constant( 'NS_EXTENSION_REGISTRATION_TEST' ) );
|
|
|
|
$expectedNamespaceNames = [ 1300 => 'ExtensionRegistrationTest' ];
|
|
$this->assertSame( $expectedNamespaceNames, $registry->getAttribute( 'ExtensionNamespaces' ) );
|
|
|
|
$this->assertArrayHasKey( 1300, $GLOBALS['wgNamespaceProtection'] );
|
|
$this->assertArrayHasKey( 1300, $GLOBALS['wgNamespaceContentModels'] );
|
|
}
|
|
|
|
public function testExportHooks() {
|
|
$manifest = [
|
|
'Hooks' => [
|
|
'AnEvent' => 'FooBarClass::onAnEvent',
|
|
'BooEvent' => 'main',
|
|
],
|
|
'HookHandler' => [
|
|
'main' => [ 'class' => 'Whatever' ]
|
|
],
|
|
];
|
|
|
|
$file = $this->makeManifestFile( $manifest );
|
|
|
|
$registry = new ExtensionRegistry();
|
|
$registry->queue( $file );
|
|
$registry->loadFromQueue();
|
|
|
|
$this->resetServices();
|
|
$hookContainer = $this->getServiceContainer()->getHookContainer();
|
|
$this->assertTrue( $hookContainer->isRegistered( 'AnEvent' ) );
|
|
$this->assertTrue( $hookContainer->isRegistered( 'BooEvent' ) );
|
|
}
|
|
|
|
public function testExportAutoload() {
|
|
global $wgAutoloadClasses;
|
|
$oldAutoloadClasses = $wgAutoloadClasses;
|
|
|
|
$manifest = [
|
|
'AutoloadClasses' => [
|
|
'TestAutoloaderClass' =>
|
|
__DIR__ . '/../../data/autoloader/TestAutoloadedClass.php',
|
|
],
|
|
'AutoloadNamespaces' => [
|
|
'Dummy\Test\Namespace\\' =>
|
|
__DIR__ . '/../../data/autoloader/psr4/',
|
|
],
|
|
'HookHandler' => [
|
|
'main' => [ 'class' => 'Whatever' ]
|
|
],
|
|
];
|
|
|
|
$file = $this->makeManifestFile( $manifest );
|
|
|
|
$registry = new ExtensionRegistry();
|
|
$registry->setCache( new HashBagOStuff() );
|
|
|
|
$registry->queue( $file );
|
|
$registry->loadFromQueue();
|
|
|
|
$this->assertArrayHasKey( 'TestAutoloaderClass', AutoLoader::getClassFiles() );
|
|
$this->assertArrayHasKey( 'Dummy\Test\Namespace\\', AutoLoader::getNamespaceDirectories() );
|
|
|
|
// Now, reset and do it again, but with the cached extension info.
|
|
// This is needed because autoloader registration is currently handled
|
|
// differently when loading from the cache (T240535).
|
|
AutoLoader::restoreState( $this->autoloaderState );
|
|
$wgAutoloadClasses = $oldAutoloadClasses;
|
|
|
|
$registry->queue( $file );
|
|
$registry->loadFromQueue();
|
|
|
|
$this->assertArrayHasKey( 'TestAutoloaderClass', AutoLoader::getClassFiles() );
|
|
$this->assertArrayHasKey( 'Dummy\Test\Namespace\\', AutoLoader::getNamespaceDirectories() );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideExportConfigToGlobals
|
|
* @dataProvider provideExportAttributesToGlobals
|
|
*/
|
|
public function testExportGlobals( $desc, $before, $manifest, $expected ) {
|
|
$this->stashMwGlobals( array_keys( $expected ) );
|
|
$this->setMwGlobals( $before );
|
|
|
|
$file = $this->makeManifestFile( $manifest );
|
|
|
|
$registry = new ExtensionRegistry();
|
|
$registry->queue( $file );
|
|
$registry->loadFromQueue();
|
|
|
|
foreach ( $expected as $name => $expectedValue ) {
|
|
$this->assertArrayHasKey( $name, $GLOBALS, $desc );
|
|
$this->assertEquals( $expectedValue, $GLOBALS[$name], $desc );
|
|
}
|
|
}
|
|
|
|
private function newSettingsBuilder(): SettingsBuilder {
|
|
$settings = new SettingsBuilder(
|
|
__DIR__,
|
|
$this->createMock( ExtensionRegistry::class ),
|
|
new ArrayConfigBuilder(),
|
|
$this->createMock( PhpIniSink::class ),
|
|
null
|
|
);
|
|
|
|
return $settings;
|
|
}
|
|
|
|
public static function callbackForTest( array $ext, SettingsBuilder $settings ) {
|
|
$settings->overrideConfigValue( 'RunCallbacksTest', 'foo' );
|
|
self::assertSame( 'CallbackTest', $ext['name'] );
|
|
}
|
|
|
|
public function testRunCallbacks() {
|
|
$manifest = [
|
|
'name' => 'CallbackTest',
|
|
'callback' => [ __CLASS__, 'callbackForTest' ],
|
|
];
|
|
|
|
$file = $this->makeManifestFile( $manifest );
|
|
|
|
$settings = $this->newSettingsBuilder();
|
|
|
|
$registry = new ExtensionRegistry();
|
|
$registry->setSettingsBuilder( $settings );
|
|
|
|
$settings->enterRegistrationStage();
|
|
$registry->queue( $file );
|
|
$registry->loadFromQueue();
|
|
|
|
$this->assertSame( 'foo', $settings->getConfig()->get( 'RunCallbacksTest' ) );
|
|
}
|
|
|
|
/**
|
|
* Provides defaults coming from extension, global values from custom settings.
|
|
* The global value should be merged on top of the default from the extension (backwards merge).
|
|
*
|
|
* @return Generator
|
|
*/
|
|
public static function provideExportConfigToGlobals() {
|
|
yield [
|
|
'Simple non-array values',
|
|
[
|
|
'mwtestFooBarConfig' => true,
|
|
'mwtestFooBarConfig2' => 'string',
|
|
],
|
|
[
|
|
'config_prefix' => 'mwtest',
|
|
'config' => [
|
|
'FooBarDefault' => [ 'value' => 1234 ],
|
|
'FooBarConfig' => [ 'value' => false ],
|
|
]
|
|
],
|
|
[
|
|
'mwtestFooBarConfig' => true,
|
|
'mwtestFooBarConfig2' => 'string',
|
|
'mwtestFooBarDefault' => 1234,
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'No global already set, simple assoc array',
|
|
[],
|
|
[
|
|
'config_prefix' => 'mwtest',
|
|
'config' => [
|
|
'DefaultOptions' => [
|
|
'value' => [
|
|
'foobar' => true,
|
|
]
|
|
]
|
|
]
|
|
],
|
|
[
|
|
'mwtestDefaultOptions' => [
|
|
'foobar' => true,
|
|
]
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'No global already set, simple assoc array, manifest version 1',
|
|
[],
|
|
[
|
|
'manifest_version' => 1,
|
|
'config' => [
|
|
'_prefix' => 'mwtest',
|
|
'SomeMap' => [
|
|
'foobar' => true,
|
|
]
|
|
]
|
|
],
|
|
[
|
|
'mwtestSomeMap' => [
|
|
'foobar' => true,
|
|
]
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'Global already set, simple assoc array, manifest version 1',
|
|
[
|
|
'mwtestSomeMap' => [
|
|
'foobar' => true,
|
|
'foo' => 'string'
|
|
],
|
|
],
|
|
[
|
|
'manifest_version' => 1,
|
|
'config' => [
|
|
'_prefix' => 'mwtest',
|
|
'SomeMap' => [
|
|
'barbaz' => 12345,
|
|
'foobar' => false,
|
|
]
|
|
]
|
|
],
|
|
[
|
|
'mwtestSomeMap' => [
|
|
'barbaz' => 12345,
|
|
'foo' => 'string',
|
|
'foobar' => true,
|
|
],
|
|
]
|
|
];
|
|
|
|
yield [
|
|
'Global already set, simple list array',
|
|
[
|
|
'mwtestList' => [ 'x', 'y', 'z' ],
|
|
],
|
|
[
|
|
'manifest_version' => 1,
|
|
'config' => [
|
|
'_prefix' => 'mwtest',
|
|
'List' => [ 'a', 'b' ]
|
|
]
|
|
],
|
|
[
|
|
'mwtestList' => [ 'a', 'b', 'x', 'y', 'z' ],
|
|
]
|
|
];
|
|
|
|
yield [
|
|
'New variable, explicit merge strategy',
|
|
[
|
|
'wgNamespacesFoo' => [
|
|
100 => true,
|
|
102 => false
|
|
],
|
|
],
|
|
[
|
|
'config' => [
|
|
'NamespacesFoo' => [
|
|
'value' => [
|
|
100 => false,
|
|
500 => true,
|
|
],
|
|
'merge_strategy' => 'array_plus',
|
|
],
|
|
]
|
|
],
|
|
[
|
|
'wgNamespacesFoo' => [
|
|
100 => true,
|
|
102 => false,
|
|
500 => true,
|
|
],
|
|
]
|
|
];
|
|
|
|
yield [
|
|
'New variable, explicit merge strategy, manifest version 1',
|
|
[
|
|
'wgNamespacesFoo' => [
|
|
100 => true,
|
|
102 => false
|
|
],
|
|
],
|
|
[
|
|
'manifest_version' => 1,
|
|
'config' => [
|
|
'NamespacesFoo' => [
|
|
100 => false,
|
|
500 => true,
|
|
ExtensionRegistry::MERGE_STRATEGY => 'array_plus',
|
|
],
|
|
]
|
|
],
|
|
[
|
|
'wgNamespacesFoo' => [
|
|
100 => true,
|
|
102 => false,
|
|
500 => true,
|
|
],
|
|
]
|
|
];
|
|
|
|
yield [
|
|
'False local setting should not be overridden by default (T100767)',
|
|
[
|
|
'wgT100767' => false,
|
|
],
|
|
[
|
|
'config' => [
|
|
'T100767' => [ 'value' => true ],
|
|
]
|
|
],
|
|
[
|
|
'wgT100767' => false,
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'test array_replace_recursive',
|
|
[
|
|
'mwtestJsonConfigs' => [
|
|
'JsonZeroConfig' => [
|
|
'namespace' => 480,
|
|
'nsName' => 'Zero',
|
|
'isLocal' => false,
|
|
'remote' => [
|
|
'username' => 'foo',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
[
|
|
'config_prefix' => 'mwtest',
|
|
'config' => [
|
|
'JsonConfigs' => [
|
|
'value' => [
|
|
'JsonZeroConfig' => [
|
|
'isLocal' => true,
|
|
],
|
|
],
|
|
'merge_strategy' => 'array_replace_recursive',
|
|
],
|
|
]
|
|
],
|
|
[
|
|
'mwtestJsonConfigs' => [
|
|
'JsonZeroConfig' => [
|
|
'namespace' => 480,
|
|
'nsName' => 'Zero',
|
|
'isLocal' => false,
|
|
'remote' => [
|
|
'username' => 'foo',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'Default doesn\'t override null',
|
|
[
|
|
'wgNullGlobal' => null,
|
|
],
|
|
[
|
|
'config' => [
|
|
'NullGlobal' => [ 'value' => 'not-null' ]
|
|
]
|
|
],
|
|
[
|
|
'wgNullGlobal' => null
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'provide_default passive case',
|
|
[
|
|
'wgFlatArray' => [],
|
|
],
|
|
[
|
|
'config' => [
|
|
'FlatArray' => [
|
|
'value' => [ 1 ],
|
|
'merge_strategy' => 'provide_default'
|
|
],
|
|
]
|
|
],
|
|
[
|
|
'wgFlatArray' => []
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'provide_default active case',
|
|
[],
|
|
[
|
|
'config' => [
|
|
'FlatArray' => [
|
|
'value' => [ 1 ],
|
|
'merge_strategy' => 'provide_default'
|
|
],
|
|
]
|
|
],
|
|
[
|
|
'wgFlatArray' => [ 1 ]
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Provide global values as default coming from core, new value from extension attribute.
|
|
* The value coming from the extension should be merged on top of the global.
|
|
*
|
|
* @return Generator
|
|
*/
|
|
public static function provideExportAttributesToGlobals() {
|
|
yield [
|
|
'AvailableRights appends to default value, per config schema',
|
|
[
|
|
'wgAvailableRights' => [
|
|
'aaa',
|
|
'bbb'
|
|
],
|
|
],
|
|
[ 'AvailableRights' => [ 'ccc', ] ],
|
|
[
|
|
// NOTE: This is backwards! Fortunately, the order in AvailableRights
|
|
// is not significant.
|
|
'wgAvailableRights' => [
|
|
'ccc',
|
|
'aaa',
|
|
'bbb',
|
|
],
|
|
]
|
|
];
|
|
|
|
yield [
|
|
'AuthManagerAutoConfig appends to default value, per top level key',
|
|
[
|
|
'wgAuthManagerAutoConfig' => [
|
|
'preauth' => [ 'default' => 'DefaultPreAuth' ],
|
|
'primaryauth' => [ 'default' => 'DefaultPrimaryAuth' ],
|
|
'secondaryauth' => [ 'default' => 'DefaultSecondaryAuth' ],
|
|
],
|
|
],
|
|
[
|
|
'AuthManagerAutoConfig' => [
|
|
'primaryauth' => [ 'my' => 'MyPrimaryAuth' ],
|
|
],
|
|
],
|
|
[
|
|
'wgAuthManagerAutoConfig' => [
|
|
'preauth' => [ 'default' => 'DefaultPreAuth' ],
|
|
'primaryauth' => [ 'default' => 'DefaultPrimaryAuth', 'my' => 'MyPrimaryAuth' ],
|
|
'secondaryauth' => [ 'default' => 'DefaultSecondaryAuth' ],
|
|
],
|
|
]
|
|
];
|
|
|
|
yield [
|
|
'No global already set, $wgHooks',
|
|
[
|
|
'wgHooks' => [],
|
|
],
|
|
[
|
|
'Hooks' => [ 'AnEvent' => 'FooBarClass::onAnEvent' ]
|
|
],
|
|
[
|
|
'wgHooks' => [
|
|
'AnEvent' => [
|
|
'FooBarClass::onAnEvent'
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'Global already set, $wgHooks',
|
|
[
|
|
'wgHooks' => [
|
|
'AnEvent' => [
|
|
'FooBarClass::onAnEvent'
|
|
],
|
|
'BooEvent' => [
|
|
'FooBarClass::onBooEvent',
|
|
],
|
|
],
|
|
],
|
|
[
|
|
'Hooks' => [ 'AnEvent' => 'BazBarClass::onAnEvent' ]
|
|
],
|
|
[
|
|
'wgHooks' => [
|
|
'AnEvent' => [
|
|
'FooBarClass::onAnEvent',
|
|
'BazBarClass::onAnEvent',
|
|
],
|
|
'BooEvent' => [
|
|
'FooBarClass::onBooEvent',
|
|
],
|
|
],
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'Entries from HookHandlers should not go into $wgHooks',
|
|
[
|
|
'wgHooks' => [],
|
|
],
|
|
[
|
|
'Hooks' => [ 'AnEvent' => 'main' ],
|
|
'HookHandlers' => [
|
|
'main' => [
|
|
'class' => 'FooBarClass',
|
|
]
|
|
],
|
|
],
|
|
[
|
|
'wgHooks' => [],
|
|
],
|
|
];
|
|
|
|
yield [
|
|
'Global already set, $wgGroupPermissions',
|
|
[
|
|
'wgGroupPermissions' => [
|
|
'sysop' => [
|
|
'something' => true,
|
|
],
|
|
'user' => [
|
|
'somethingtwo' => true,
|
|
]
|
|
],
|
|
],
|
|
[
|
|
'GroupPermissions' => [
|
|
'customgroup' => [
|
|
'right' => true,
|
|
],
|
|
'user' => [
|
|
'right' => true,
|
|
'somethingtwo' => false,
|
|
'nonduplicated' => true,
|
|
],
|
|
],
|
|
],
|
|
[
|
|
'wgGroupPermissions' => [
|
|
'customgroup' => [
|
|
'right' => true,
|
|
],
|
|
'sysop' => [
|
|
'something' => true,
|
|
],
|
|
'user' => [
|
|
// NOTE: somethingtwo should be false here, since the value from
|
|
// the extension should override the core default!
|
|
// See e.g. https://www.mediawiki.org/wiki/Topic:W2ttbedo3apzno4w
|
|
// and https://phabricator.wikimedia.org/T98347#2589540.
|
|
'somethingtwo' => true,
|
|
'right' => true,
|
|
'nonduplicated' => true,
|
|
]
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array $manifest
|
|
*
|
|
* @return string
|
|
*/
|
|
private function makeManifestFile( array $manifest ): string {
|
|
$manifest += [
|
|
'name' => 'Test',
|
|
'manifest_version' => 2,
|
|
'config' => [],
|
|
'callbacks' => [],
|
|
'defines' => [],
|
|
'credits' => [],
|
|
'attributes' => [],
|
|
'autoloaderPaths' => []
|
|
];
|
|
|
|
$file = $this->getNewTempFile();
|
|
file_put_contents( $file, json_encode( $manifest ) );
|
|
return $file;
|
|
}
|
|
|
|
public function testExportAutoloaderWithPsr4Namespaces() {
|
|
$dir = __DIR__ . '/../../data/registration';
|
|
$registry = new ExtensionRegistry();
|
|
$data = $registry->readFromQueue( [
|
|
"{$dir}/autoload_namespaces.json" => 1
|
|
] );
|
|
|
|
$access = TestingAccessWrapper::newFromObject( $registry );
|
|
$access->exportExtractedData( $data );
|
|
|
|
$this->assertTrue(
|
|
class_exists( 'Test\\MediaWiki\\AutoLoader\\TestFooBar' ),
|
|
"Registry initializes Autoloader from AutoloadNamespaces"
|
|
);
|
|
}
|
|
|
|
}
|