If not specified, going forward all modules which do not define targets should be assumed to be mobile AND desktop. I think this is a fair expectation given that most new features should be built with mobile in mind. This also avoids the issue where features are unexpectedly not shipped to mobile. The work in T324723 has ensured that all modules that need to remain desktop only have been marked as such. After the audit, we can be reasonably confident that enabling the remaining modules is okay, given these modules run in isolated parts of the UI (not on every page view). I'm assuming CI is catching all offenders here. If not, any impacted modules would require a trivial update to explicitly define modules as mobile only. This would allow us to see clearly which modules are depending on the target system and strategize around updating them. Once this patch is merged we can focus on fixing the modules we've identified as mobile or desktop targeted and finally dismantling the targets system altogether. Bug: T127268 Change-Id: Ia062ff2d8b8732b0d3498c1a614bbf6a3e3a7ddb
1273 lines
34 KiB
PHP
1273 lines
34 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\Registration;
|
|
|
|
use Exception;
|
|
use ExtensionProcessor;
|
|
use ExtensionRegistry;
|
|
use FormatJson;
|
|
use InvalidArgumentException;
|
|
use MediaWikiUnitTestCase;
|
|
use RuntimeException;
|
|
use UnexpectedValueException;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @covers ExtensionProcessor
|
|
*/
|
|
class ExtensionProcessorTest extends MediaWikiUnitTestCase {
|
|
|
|
private $extensionPath, $dirname;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
$this->extensionPath = self::getExtensionPath();
|
|
$this->dirname = dirname( $this->extensionPath );
|
|
}
|
|
|
|
private static function getExtensionPath() {
|
|
return __DIR__ . '/fixtures/FooBar/extension.json';
|
|
}
|
|
|
|
/**
|
|
* 'name' is absolutely required
|
|
*
|
|
* @var array
|
|
*/
|
|
public static $default = [
|
|
'name' => 'FooBar',
|
|
];
|
|
|
|
/**
|
|
* 'name' is absolutely required, and sometimes we require two distinct ones...
|
|
* @var array
|
|
*/
|
|
public static $default2 = [
|
|
'name' => 'FooBar2',
|
|
];
|
|
|
|
public function testExtractInfo() {
|
|
// Test that attributes that begin with @ are ignored
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, self::$default + [
|
|
'@metadata' => [ 'foobarbaz' ],
|
|
'AnAttribute' => [ 'omg' ],
|
|
'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
|
|
'AutoloadNamespaces' => [ '\Foo\Bar\\' => 'includes/foo/bar/' ],
|
|
'SpecialPages' => [ 'Foo' => 'SpecialFoo' ],
|
|
'callback' => 'FooBar::onRegistration',
|
|
], 1 );
|
|
|
|
$extracted = $processor->getExtractedInfo();
|
|
$attributes = $extracted['attributes'];
|
|
$this->assertArrayHasKey( 'AnAttribute', $attributes );
|
|
$this->assertArrayNotHasKey( '@metadata', $attributes );
|
|
$this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
|
|
$this->assertArrayNotHasKey( 'AutoloadNamespaces', $attributes );
|
|
$this->assertArrayHasKey( 'autoloaderPaths', $extracted );
|
|
$this->assertArrayHasKey( 'autoloaderClasses', $extracted );
|
|
$this->assertArrayHasKey( 'autoloaderNS', $extracted );
|
|
$this->assertSame(
|
|
[ 'FooBar' => 'FooBar::onRegistration' ],
|
|
$extracted['callbacks']
|
|
);
|
|
$this->assertSame(
|
|
[ 'Foo' => 'SpecialFoo' ],
|
|
$extracted['globals']['wgSpecialPages']
|
|
);
|
|
}
|
|
|
|
public function testExtractAutoload() {
|
|
// Test that attributes that begin with @ are ignored
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, self::$default + [
|
|
'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
|
|
'AutoloadNamespaces' => [ '\Foo\Bar\\' => 'includes/foo/bar/' ],
|
|
'TestAutoloadClasses' => [ 'FooBarTest' => 'tests/FooBarTest.php' ],
|
|
'TestAutoloadNamespaces' => [ '\Foo\Bar\Test\\' => 'tests/foo/bar/' ],
|
|
'load_composer_autoloader' => true,
|
|
], 1 );
|
|
|
|
$extracted = $processor->getExtractedInfo();
|
|
$this->assertArrayHasKey( 'autoloaderPaths', $extracted );
|
|
$this->assertArrayHasKey( 'autoloaderClasses', $extracted );
|
|
$this->assertArrayHasKey( 'autoloaderNS', $extracted );
|
|
$this->assertSame(
|
|
[ 'FooBar' => $this->dirname . '/includes/FooBar.php' ],
|
|
$extracted['autoloaderClasses']
|
|
);
|
|
$this->assertSame(
|
|
[ '\Foo\Bar\\' => $this->dirname . '/includes/foo/bar/' ],
|
|
$extracted['autoloaderNS']
|
|
);
|
|
$this->assertSame(
|
|
[ $this->dirname . '/vendor/autoload.php' ],
|
|
$extracted['autoloaderPaths']
|
|
);
|
|
|
|
$extracted = $processor->getExtractedInfo( true );
|
|
$this->assertArrayHasKey( 'autoloaderPaths', $extracted );
|
|
$this->assertArrayHasKey( 'autoloaderClasses', $extracted );
|
|
$this->assertArrayHasKey( 'autoloaderNS', $extracted );
|
|
$this->assertSame(
|
|
[
|
|
'FooBar' => $this->dirname . '/includes/FooBar.php',
|
|
'FooBarTest' => $this->dirname . '/tests/FooBarTest.php'
|
|
],
|
|
$extracted['autoloaderClasses']
|
|
);
|
|
$this->assertSame(
|
|
[
|
|
'\Foo\Bar\\' => $this->dirname . '/includes/foo/bar/',
|
|
'\Foo\Bar\Test\\' => $this->dirname . '/tests/foo/bar/'
|
|
],
|
|
$extracted['autoloaderNS']
|
|
);
|
|
$this->assertSame(
|
|
[ $this->dirname . '/vendor/autoload.php' ],
|
|
$extracted['autoloaderPaths']
|
|
);
|
|
}
|
|
|
|
public function testExtractSkins() {
|
|
$this->expectDeprecation();
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, self::$default + [
|
|
'ValidSkinNames' => [
|
|
'test-vector' => [
|
|
'class' => 'SkinTestVector',
|
|
],
|
|
'test-vector-empty-args' => [
|
|
'class' => 'SkinTestVector',
|
|
'args' => []
|
|
],
|
|
'test-vector-empty-options' => [
|
|
'class' => 'SkinTestVector',
|
|
'args' => [
|
|
[]
|
|
]
|
|
],
|
|
'test-vector-core-relative' => [
|
|
'class' => 'SkinTestVector',
|
|
'args' => [
|
|
[
|
|
'templateDirectory' => 'skins/Vector/templates',
|
|
]
|
|
]
|
|
],
|
|
'test-vector-skin-relative' => [
|
|
'class' => 'SkinTestVector',
|
|
'args' => [
|
|
[
|
|
'templateDirectory' => 'templates',
|
|
]
|
|
]
|
|
],
|
|
]
|
|
], 1 );
|
|
$extracted = $processor->getExtractedInfo();
|
|
$validSkins = $extracted['globals']['wgValidSkinNames'];
|
|
|
|
$this->assertArrayHasKey( 'test-vector', $validSkins );
|
|
$this->assertArrayHasKey( 'test-vector-core-relative', $validSkins );
|
|
$this->assertArrayHasKey( 'test-vector-empty-args', $validSkins );
|
|
$this->assertArrayHasKey( 'test-vector-skin-relative', $validSkins );
|
|
$this->assertSame(
|
|
$this->dirname . '/templates',
|
|
$validSkins['test-vector-empty-options']['args'][0]['templateDirectory'],
|
|
'A sensible default is provided.'
|
|
);
|
|
$this->assertSame(
|
|
'skins/Vector/templates',
|
|
$validSkins['test-vector-core-relative']['args'][0]['templateDirectory'],
|
|
'unmodified'
|
|
);
|
|
$this->assertSame(
|
|
$this->dirname . '/templates',
|
|
$validSkins['test-vector-skin-relative']['args'][0]['templateDirectory'],
|
|
'modified'
|
|
);
|
|
}
|
|
|
|
public function testExtractNamespaces() {
|
|
// Test that namespace IDs defined in extension.json can be overwritten locally
|
|
if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
|
|
define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
|
|
}
|
|
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, self::$default + [
|
|
'namespaces' => [
|
|
[
|
|
'id' => 332200,
|
|
'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
|
|
'name' => 'Test_A',
|
|
'defaultcontentmodel' => 'TestModel',
|
|
'gender' => [
|
|
'male' => 'Male test',
|
|
'female' => 'Female test',
|
|
],
|
|
'subpages' => true,
|
|
'content' => true,
|
|
'protection' => 'userright',
|
|
],
|
|
[ // Test_X will use ID 123456 not 334400
|
|
'id' => 334400,
|
|
'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
|
|
'name' => 'Test_X',
|
|
'defaultcontentmodel' => 'TestModel'
|
|
],
|
|
]
|
|
], 1 );
|
|
|
|
$extracted = $processor->getExtractedInfo();
|
|
|
|
$this->assertArrayHasKey(
|
|
'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
|
|
$extracted['defines']
|
|
);
|
|
$this->assertArrayHasKey(
|
|
'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
|
|
$extracted['defines']
|
|
);
|
|
|
|
$this->assertSame(
|
|
123456,
|
|
$extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X']
|
|
);
|
|
|
|
$this->assertSame(
|
|
332200,
|
|
$extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A']
|
|
);
|
|
|
|
$this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
|
|
$this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
|
|
$this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
|
|
$this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
|
|
|
|
$this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
|
|
$this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
|
|
$this->assertSame(
|
|
[ 'male' => 'Male test', 'female' => 'Female test' ],
|
|
$extracted['globals']['wgExtraGenderNamespaces'][332200]
|
|
);
|
|
// A has subpages, X does not
|
|
$this->assertTrue( $extracted['globals']['wgNamespacesWithSubpages'][332200] );
|
|
$this->assertArrayNotHasKey( 123456, $extracted['globals']['wgNamespacesWithSubpages'] );
|
|
}
|
|
|
|
public function provideMixedStyleHooks() {
|
|
// Format:
|
|
// Content in extension.json
|
|
// Expected wgHooks
|
|
// Expected Hooks
|
|
return [
|
|
[
|
|
[
|
|
'Hooks' => [ 'FooBaz' => [
|
|
[ 'handler' => 'HandlerObjectCallback' ],
|
|
[ 'handler' => 'HandlerObjectCallback', 'deprecated' => true ],
|
|
'HandlerObjectCallback',
|
|
[ 'FooClass', 'FooMethod' ],
|
|
'GlobalLegacyFunction',
|
|
'FooClass',
|
|
"FooClass::staticMethod"
|
|
] ],
|
|
'HookHandlers' => [
|
|
'HandlerObjectCallback' => [ 'class' => 'FooClass', 'services' => [] ]
|
|
]
|
|
] + self::$default,
|
|
[
|
|
'FooBaz' => [
|
|
[ 'FooClass', 'FooMethod' ],
|
|
'GlobalLegacyFunction',
|
|
'FooClass',
|
|
'FooClass::staticMethod'
|
|
]
|
|
] + [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ],
|
|
[
|
|
'FooBaz' => [
|
|
[
|
|
'handler' => [
|
|
'class' => 'FooClass',
|
|
'services' => [],
|
|
'name' => 'FooBar-HandlerObjectCallback'
|
|
],
|
|
'extensionPath' => $this->getExtensionPath()
|
|
],
|
|
[
|
|
'handler' => [
|
|
'class' => 'FooClass',
|
|
'services' => [],
|
|
'name' => 'FooBar-HandlerObjectCallback'
|
|
],
|
|
'deprecated' => true,
|
|
'extensionPath' => $this->getExtensionPath()
|
|
],
|
|
[
|
|
'handler' => [
|
|
'class' => 'FooClass',
|
|
'services' => [],
|
|
'name' => 'FooBar-HandlerObjectCallback'
|
|
],
|
|
'extensionPath' => $this->getExtensionPath()
|
|
]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
public function provideNonLegacyHooks() {
|
|
// Format:
|
|
// Current Hooks attribute
|
|
// Content in extension.json
|
|
// Expected Hooks attribute
|
|
return [
|
|
// Hook for "FooBaz": object with handler attribute
|
|
[
|
|
[ 'FooBaz' => [ 'PriorCallback' ] ],
|
|
[
|
|
'Hooks' => [ 'FooBaz' => [ 'handler' => 'HandlerObjectCallback', 'deprecated' => true ] ],
|
|
'HookHandlers' => [
|
|
'HandlerObjectCallback' => [
|
|
'class' => 'FooClass',
|
|
'services' => [],
|
|
'name' => 'FooBar-HandlerObjectCallback'
|
|
]
|
|
]
|
|
] + self::$default,
|
|
[ 'FooBaz' =>
|
|
[
|
|
'PriorCallback',
|
|
[
|
|
'handler' => [
|
|
'class' => 'FooClass',
|
|
'services' => [],
|
|
'name' => 'FooBar-HandlerObjectCallback'
|
|
],
|
|
'deprecated' => true,
|
|
'extensionPath' => $this->getExtensionPath()
|
|
]
|
|
]
|
|
],
|
|
[]
|
|
],
|
|
// Hook for "FooBaz": string corresponding to a handler definition
|
|
[
|
|
[ 'FooBaz' => [ 'PriorCallback' ] ],
|
|
[
|
|
'Hooks' => [ 'FooBaz' => [ 'HandlerObjectCallback' ] ],
|
|
'HookHandlers' => [
|
|
'HandlerObjectCallback' => [ 'class' => 'FooClass', 'services' => [] ],
|
|
]
|
|
] + self::$default,
|
|
[ 'FooBaz' =>
|
|
[
|
|
'PriorCallback',
|
|
[
|
|
'handler' => [
|
|
'class' => 'FooClass',
|
|
'services' => [],
|
|
'name' => 'FooBar-HandlerObjectCallback'
|
|
],
|
|
'extensionPath' => $this->getExtensionPath()
|
|
],
|
|
]
|
|
],
|
|
[]
|
|
],
|
|
// Hook for "FooBaz", string corresponds to handler def. and object with handler attribute
|
|
[
|
|
[ 'FooBaz' => [ 'PriorCallback' ] ],
|
|
[
|
|
'Hooks' => [ 'FooBaz' => [
|
|
[ 'handler' => 'HandlerObjectCallback', 'deprecated' => true ],
|
|
'HandlerObjectCallback2'
|
|
] ],
|
|
'HookHandlers' => [
|
|
'HandlerObjectCallback2' => [ 'class' => 'FooClass', 'services' => [] ],
|
|
'HandlerObjectCallback' => [ 'class' => 'FooClass', 'services' => [] ],
|
|
]
|
|
] + self::$default,
|
|
[ 'FooBaz' =>
|
|
[
|
|
'PriorCallback',
|
|
[
|
|
'handler' => [
|
|
'name' => 'FooBar-HandlerObjectCallback',
|
|
'class' => 'FooClass',
|
|
'services' => []
|
|
],
|
|
'deprecated' => true,
|
|
'extensionPath' => $this->getExtensionPath()
|
|
],
|
|
[
|
|
'handler' => [
|
|
'name' => 'FooBar-HandlerObjectCallback2',
|
|
'class' => 'FooClass',
|
|
'services' => [],
|
|
],
|
|
'extensionPath' => $this->getExtensionPath()
|
|
]
|
|
]
|
|
],
|
|
[]
|
|
],
|
|
// Hook for "FooBaz": string corresponding to a new-style handler definition
|
|
// and legacy style object and method array
|
|
[
|
|
[ 'FooBaz' => [ 'PriorCallback' ] ],
|
|
[
|
|
'Hooks' => [ 'FooBaz' => [
|
|
'HandlerObjectCallback',
|
|
[ 'FooClass', 'FooMethod ' ]
|
|
] ],
|
|
'HookHandlers' => [
|
|
'HandlerObjectCallback' => [ 'class' => 'FooClass', 'services' => [] ],
|
|
]
|
|
] + self::$default,
|
|
[ 'FooBaz' =>
|
|
[
|
|
'PriorCallback',
|
|
[
|
|
'handler' => [
|
|
'name' => 'FooBar-HandlerObjectCallback',
|
|
'class' => 'FooClass',
|
|
'services' => []
|
|
],
|
|
'extensionPath' => $this->getExtensionPath()
|
|
],
|
|
]
|
|
],
|
|
[ 'FooClass', 'FooMethod' ]
|
|
]
|
|
];
|
|
}
|
|
|
|
public function provideLegacyHooks() {
|
|
$merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
|
|
// Format:
|
|
// Current $wgHooks
|
|
// Content in extension.json
|
|
// Expected value of $wgHooks
|
|
return [
|
|
// No hooks
|
|
[
|
|
[],
|
|
self::$default,
|
|
$merge,
|
|
],
|
|
// No current hooks, adding one for "FooBaz" in string format
|
|
[
|
|
[],
|
|
[ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
|
|
[ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
|
|
],
|
|
// Hook for "FooBaz", adding another one
|
|
[
|
|
[ 'FooBaz' => [ 'PriorCallback' ] ],
|
|
[ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
|
|
[ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
|
|
],
|
|
// No current hooks, adding one for "FooBaz" in verbose array format
|
|
[
|
|
[],
|
|
[ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
|
|
[ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
|
|
],
|
|
// Hook for "BarBaz", adding one for "FooBaz"
|
|
[
|
|
[ 'BarBaz' => [ 'BarBazCallback' ] ],
|
|
[ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
|
|
[
|
|
'BarBaz' => [ 'BarBazCallback' ],
|
|
'FooBaz' => [ 'FooBazCallback' ],
|
|
] + $merge,
|
|
],
|
|
// Callbacks for FooBaz wrapped in an array
|
|
[
|
|
[],
|
|
[ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
|
|
[
|
|
'FooBaz' => [ 'Callback1' ],
|
|
] + $merge,
|
|
],
|
|
// Multiple callbacks for FooBaz hook
|
|
[
|
|
[],
|
|
[ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
|
|
[
|
|
'FooBaz' => [ 'Callback1', 'Callback2' ],
|
|
] + $merge,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideNonLegacyHooks
|
|
*/
|
|
public function testNonLegacyHooks( $pre, $info, $expected ) {
|
|
$processor = new MockExtensionProcessor( [ 'attributes' => [ 'Hooks' => $pre ] ] );
|
|
$processor->extractInfo( $this->extensionPath, $info, 1 );
|
|
$extracted = $processor->getExtractedInfo();
|
|
$this->assertEquals( $expected, $extracted['attributes']['Hooks'] );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideMixedStyleHooks
|
|
*/
|
|
public function testMixedStyleHooks( $info, $expectedWgHooks, $expectedNewHooks ) {
|
|
$processor = new MockExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, $info, 1 );
|
|
$extracted = $processor->getExtractedInfo();
|
|
$this->assertEquals( $expectedWgHooks, $extracted['globals']['wgHooks'] );
|
|
$this->assertEquals( $expectedNewHooks, $extracted['attributes']['Hooks'] );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideLegacyHooks
|
|
*/
|
|
public function testLegacyHooks( $pre, $info, $expected ) {
|
|
$preset = [ 'globals' => [ 'wgHooks' => $pre ] ];
|
|
$processor = new MockExtensionProcessor( $preset );
|
|
$processor->extractInfo( $this->extensionPath, $info, 1 );
|
|
$extracted = $processor->getExtractedInfo();
|
|
$this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
|
|
}
|
|
|
|
public function testRegisterHandlerWithoutDefinition() {
|
|
$info = [
|
|
'Hooks' => [ 'FooBaz' => [ 'handler' => 'NoHandlerDefinition' ] ],
|
|
'HookHandlers' => []
|
|
] + self::$default;
|
|
$processor = new MockExtensionProcessor();
|
|
$this->expectException( UnexpectedValueException::class );
|
|
$this->expectExceptionMessage(
|
|
'Missing handler definition for FooBaz in HookHandlers attribute'
|
|
);
|
|
$processor->extractInfo( $this->extensionPath, $info, 1 );
|
|
$processor->getExtractedInfo();
|
|
}
|
|
|
|
public function testExtractConfig1() {
|
|
$processor = new ExtensionProcessor();
|
|
$info = [
|
|
'config' => [
|
|
'Bar' => 'somevalue',
|
|
'Foo' => 10,
|
|
'@IGNORED' => 'yes',
|
|
],
|
|
] + self::$default;
|
|
$info2 = [
|
|
'config' => [
|
|
'_prefix' => 'eg',
|
|
'Bar' => 'somevalue'
|
|
],
|
|
] + self::$default2;
|
|
$processor->extractInfo( $this->extensionPath, $info, 1 );
|
|
$processor->extractInfo( $this->extensionPath, $info2, 1 );
|
|
$extracted = $processor->getExtractedInfo();
|
|
$this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
|
|
$this->assertEquals( 10, $extracted['globals']['wgFoo'] );
|
|
$this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
|
|
// Custom prefix:
|
|
$this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
|
|
}
|
|
|
|
public function testExtractConfig2() {
|
|
$processor = new ExtensionProcessor();
|
|
$info = [
|
|
'config' => [
|
|
'Bar' => [ 'value' => 'somevalue' ],
|
|
'Foo' => [ 'value' => 10 ],
|
|
'Path' => [ 'value' => 'foo.txt', 'path' => true ],
|
|
'PathArray' => [ 'value' => [ 'foo.bar', 'bar.foo', 'bar/foo.txt' ], 'path' => true ],
|
|
'Namespaces' => [
|
|
'value' => [
|
|
'10' => true,
|
|
'12' => false,
|
|
],
|
|
'merge_strategy' => 'array_plus',
|
|
],
|
|
],
|
|
] + self::$default;
|
|
$info2 = [
|
|
'config' => [
|
|
'Bar' => [ 'value' => 'somevalue' ],
|
|
],
|
|
'config_prefix' => 'eg',
|
|
] + self::$default2;
|
|
$processor->extractInfo( $this->extensionPath, $info, 2 );
|
|
$processor->extractInfo( $this->extensionPath, $info2, 2 );
|
|
$extracted = $processor->getExtractedInfo();
|
|
$this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
|
|
$this->assertEquals( 10, $extracted['globals']['wgFoo'] );
|
|
$this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
|
|
$this->assertEquals(
|
|
[
|
|
"{$this->dirname}/foo.bar",
|
|
"{$this->dirname}/bar.foo",
|
|
"{$this->dirname}/bar/foo.txt"
|
|
],
|
|
$extracted['globals']['wgPathArray']
|
|
);
|
|
// Custom prefix:
|
|
$this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
|
|
$this->assertSame(
|
|
[ 10 => true, 12 => false, ExtensionRegistry::MERGE_STRATEGY => 'array_plus' ],
|
|
$extracted['globals']['wgNamespaces']
|
|
);
|
|
}
|
|
|
|
public function testDuplicateConfigKey1() {
|
|
$processor = new ExtensionProcessor();
|
|
$info = [
|
|
'config' => [
|
|
'Bar' => '',
|
|
]
|
|
] + self::$default;
|
|
$info2 = [
|
|
'config' => [
|
|
'Bar' => 'g',
|
|
],
|
|
] + self::$default2;
|
|
$this->expectException( RuntimeException::class );
|
|
$processor->extractInfo( $this->extensionPath, $info, 1 );
|
|
$processor->extractInfo( $this->extensionPath, $info2, 1 );
|
|
}
|
|
|
|
public function testDuplicateConfigKey2() {
|
|
$processor = new ExtensionProcessor();
|
|
$info = [
|
|
'config' => [
|
|
'Bar' => [ 'value' => 'somevalue' ],
|
|
]
|
|
] + self::$default;
|
|
$info2 = [
|
|
'config' => [
|
|
'Bar' => [ 'value' => 'somevalue' ],
|
|
],
|
|
] + self::$default2;
|
|
$this->expectException( RuntimeException::class );
|
|
$processor->extractInfo( $this->extensionPath, $info, 2 );
|
|
$processor->extractInfo( $this->extensionPath, $info2, 2 );
|
|
}
|
|
|
|
public static function provideExtractExtensionMessagesFiles() {
|
|
$dir = dirname( self::getExtensionPath() );
|
|
return [
|
|
[
|
|
[ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
|
|
[ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . '/FooBar.alias.php' ] ]
|
|
],
|
|
[
|
|
[
|
|
'ExtensionMessagesFiles' => [
|
|
'FooBarAlias' => 'FooBar.alias.php',
|
|
'FooBarMagic' => 'FooBar.magic.i18n.php',
|
|
],
|
|
],
|
|
[
|
|
'wgExtensionMessagesFiles' => [
|
|
'FooBarAlias' => $dir . '/FooBar.alias.php',
|
|
'FooBarMagic' => $dir . '/FooBar.magic.i18n.php',
|
|
],
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideExtractExtensionMessagesFiles
|
|
*/
|
|
public function testExtractExtensionMessagesFiles( $input, $expected ) {
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, $input + self::$default, 1 );
|
|
$out = $processor->getExtractedInfo();
|
|
foreach ( $expected as $key => $value ) {
|
|
$this->assertEquals( $value, $out['globals'][$key] );
|
|
}
|
|
}
|
|
|
|
public static function provideExtractMessagesDirs() {
|
|
$dir = dirname( self::getExtensionPath() );
|
|
return [
|
|
[
|
|
[ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
|
|
[ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . '/i18n' ] ] ]
|
|
],
|
|
[
|
|
[ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
|
|
[ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . '/i18n', $dir . '/foobar' ] ] ]
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideExtractMessagesDirs
|
|
*/
|
|
public function testExtractMessagesDirs( $input, $expected ) {
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, $input + self::$default, 1 );
|
|
$out = $processor->getExtractedInfo();
|
|
foreach ( $expected as $key => $value ) {
|
|
$this->assertEquals( $value, $out['globals'][$key] );
|
|
}
|
|
}
|
|
|
|
public function testExtractCredits() {
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, self::$default, 1 );
|
|
$this->expectException( Exception::class );
|
|
$processor->extractInfo( $this->extensionPath, self::$default, 1 );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideExtractResourceLoaderModules
|
|
*/
|
|
public function testExtractResourceLoaderModules(
|
|
$input,
|
|
array $expectedGlobals,
|
|
array $expectedAttribs = []
|
|
) {
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo( $this->extensionPath, $input + self::$default, 1 );
|
|
$out = $processor->getExtractedInfo();
|
|
foreach ( $expectedGlobals as $key => $value ) {
|
|
$this->assertEquals( $value, $out['globals'][$key] );
|
|
}
|
|
foreach ( $expectedAttribs as $key => $value ) {
|
|
$this->assertEquals( $value, $out['attributes'][$key] );
|
|
}
|
|
}
|
|
|
|
public static function provideExtractResourceLoaderModules() {
|
|
$dir = dirname( self::getExtensionPath() );
|
|
return [
|
|
// Generic module with localBasePath/remoteExtPath specified
|
|
[
|
|
// Input
|
|
[
|
|
'ResourceModules' => [
|
|
'test.foo' => [
|
|
'styles' => 'foobar.js',
|
|
'localBasePath' => '',
|
|
'remoteExtPath' => 'FooBar',
|
|
],
|
|
],
|
|
],
|
|
// Expected
|
|
[],
|
|
[
|
|
'ResourceModules' => [
|
|
'test.foo' => [
|
|
'styles' => 'foobar.js',
|
|
'localBasePath' => $dir,
|
|
'remoteExtPath' => 'FooBar',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
// ResourceFileModulePaths specified:
|
|
[
|
|
// Input
|
|
[
|
|
'ResourceFileModulePaths' => [
|
|
'localBasePath' => 'modules',
|
|
'remoteExtPath' => 'FooBar/modules',
|
|
],
|
|
'ResourceModules' => [
|
|
// No paths
|
|
'test.foo' => [
|
|
'styles' => 'foo.js',
|
|
],
|
|
// Different paths set
|
|
'test.bar' => [
|
|
'styles' => 'bar.js',
|
|
'localBasePath' => 'subdir',
|
|
'remoteExtPath' => 'FooBar/subdir',
|
|
],
|
|
// Custom class with no paths set
|
|
'test.class' => [
|
|
'class' => 'FooBarModule',
|
|
'extra' => 'argument',
|
|
],
|
|
// Custom class with a localBasePath
|
|
'test.class.with.path' => [
|
|
'class' => 'FooBarPathModule',
|
|
'extra' => 'argument',
|
|
'localBasePath' => '',
|
|
]
|
|
],
|
|
],
|
|
// Expected
|
|
[],
|
|
[
|
|
'ResourceModules' => [
|
|
'test.foo' => [
|
|
'styles' => 'foo.js',
|
|
'localBasePath' => "$dir/modules",
|
|
'remoteExtPath' => 'FooBar/modules',
|
|
],
|
|
'test.bar' => [
|
|
'styles' => 'bar.js',
|
|
'localBasePath' => "$dir/subdir",
|
|
'remoteExtPath' => 'FooBar/subdir',
|
|
],
|
|
'test.class' => [
|
|
'class' => 'FooBarModule',
|
|
'extra' => 'argument',
|
|
'localBasePath' => "$dir/modules",
|
|
'remoteExtPath' => 'FooBar/modules',
|
|
],
|
|
'test.class.with.path' => [
|
|
'class' => 'FooBarPathModule',
|
|
'extra' => 'argument',
|
|
'localBasePath' => $dir,
|
|
'remoteExtPath' => 'FooBar/modules',
|
|
]
|
|
],
|
|
],
|
|
],
|
|
// ResourceModuleSkinStyles with file module paths
|
|
[
|
|
// Input
|
|
[
|
|
'ResourceFileModulePaths' => [
|
|
'localBasePath' => '',
|
|
'remoteSkinPath' => 'FooBar',
|
|
],
|
|
'ResourceModuleSkinStyles' => [
|
|
'foobar' => [
|
|
'test.foo' => 'foo.css',
|
|
]
|
|
],
|
|
],
|
|
// Expected
|
|
[],
|
|
[
|
|
'ResourceModuleSkinStyles' => [
|
|
'foobar' => [
|
|
'test.foo' => 'foo.css',
|
|
'localBasePath' => $dir,
|
|
'remoteSkinPath' => 'FooBar',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
// ResourceModuleSkinStyles with file module paths and an override
|
|
[
|
|
// Input
|
|
[
|
|
'ResourceFileModulePaths' => [
|
|
'localBasePath' => '',
|
|
'remoteSkinPath' => 'FooBar',
|
|
],
|
|
'ResourceModuleSkinStyles' => [
|
|
'foobar' => [
|
|
'test.foo' => 'foo.css',
|
|
'remoteSkinPath' => 'BarFoo'
|
|
],
|
|
],
|
|
],
|
|
// Expected
|
|
[],
|
|
[
|
|
'ResourceModuleSkinStyles' => [
|
|
'foobar' => [
|
|
'test.foo' => 'foo.css',
|
|
'localBasePath' => $dir,
|
|
'remoteSkinPath' => 'BarFoo',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
'QUnit test module' => [
|
|
// Input
|
|
[
|
|
'QUnitTestModule' => [
|
|
'localBasePath' => '',
|
|
'remoteExtPath' => 'Foo',
|
|
'scripts' => 'bar.js',
|
|
],
|
|
],
|
|
// Expected
|
|
[],
|
|
[
|
|
'QUnitTestModules' => [
|
|
'test.FooBar' => [
|
|
'localBasePath' => $dir,
|
|
'remoteExtPath' => 'Foo',
|
|
'scripts' => 'bar.js',
|
|
'targets' => [ 'test' ],
|
|
],
|
|
],
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
public static function provideSetToGlobal() {
|
|
return [
|
|
[
|
|
[ 'wgAPIModules', 'wgAvailableRights' ],
|
|
[],
|
|
[
|
|
'APIModules' => [ 'foobar' => 'ApiFooBar' ],
|
|
'AvailableRights' => [ 'foobar', 'unfoobar' ],
|
|
],
|
|
[
|
|
'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
|
|
'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
|
|
],
|
|
],
|
|
[
|
|
[ 'wgAPIModules', 'wgAvailableRights' ],
|
|
[
|
|
'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
|
|
'wgAvailableRights' => [ 'barbaz' ]
|
|
],
|
|
[
|
|
'APIModules' => [ 'foobar' => 'ApiFooBar' ],
|
|
'AvailableRights' => [ 'foobar', 'unfoobar' ],
|
|
],
|
|
[
|
|
'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
|
|
'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
|
|
],
|
|
],
|
|
[
|
|
[ 'wgGroupPermissions' ],
|
|
[
|
|
'wgGroupPermissions' => [
|
|
'sysop' => [ 'delete' ]
|
|
],
|
|
],
|
|
[
|
|
'GroupPermissions' => [
|
|
'sysop' => [ 'undelete' ],
|
|
'user' => [ 'edit' ]
|
|
],
|
|
],
|
|
[
|
|
'wgGroupPermissions' => [
|
|
'sysop' => [ 'delete', 'undelete' ],
|
|
'user' => [ 'edit' ]
|
|
],
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Attributes under manifest_version 2
|
|
*/
|
|
public function testExtractAttributes() {
|
|
$processor = new ExtensionProcessor();
|
|
// Load FooBar extension
|
|
$processor->extractInfo( $this->extensionPath, self::$default, 2 );
|
|
$processor->extractInfo(
|
|
$this->extensionPath,
|
|
[
|
|
'name' => 'Baz',
|
|
'attributes' => [
|
|
// Loaded
|
|
'FooBar' => [
|
|
'Plugins' => [
|
|
'ext.baz.foobar',
|
|
],
|
|
],
|
|
// Not loaded
|
|
'FizzBuzz' => [
|
|
'MorePlugins' => [
|
|
'ext.baz.fizzbuzz',
|
|
],
|
|
],
|
|
],
|
|
],
|
|
2
|
|
);
|
|
|
|
$info = $processor->getExtractedInfo();
|
|
$this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
|
|
$this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
|
|
$this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
|
|
}
|
|
|
|
/**
|
|
* Attributes under manifest_version 1
|
|
*/
|
|
public function testAttributes1() {
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo(
|
|
$this->extensionPath,
|
|
[
|
|
'FooBarPlugins' => [
|
|
'ext.baz.foobar',
|
|
],
|
|
'FizzBuzzMorePlugins' => [
|
|
'ext.baz.fizzbuzz',
|
|
],
|
|
] + self::$default,
|
|
1
|
|
);
|
|
$processor->extractInfo(
|
|
$this->extensionPath,
|
|
[
|
|
'FizzBuzzMorePlugins' => [
|
|
'ext.bar.fizzbuzz',
|
|
]
|
|
] + self::$default2,
|
|
1
|
|
);
|
|
|
|
$info = $processor->getExtractedInfo();
|
|
$this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
|
|
$this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
|
|
$this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
|
|
$this->assertSame(
|
|
[ 'ext.baz.fizzbuzz', 'ext.bar.fizzbuzz' ],
|
|
$info['attributes']['FizzBuzzMorePlugins']
|
|
);
|
|
}
|
|
|
|
public function testAttributes1_notarray() {
|
|
$processor = new ExtensionProcessor();
|
|
$this->expectException( InvalidArgumentException::class );
|
|
$this->expectExceptionMessage(
|
|
"The value for 'FooBarPlugins' should be an array (from {$this->extensionPath})"
|
|
);
|
|
$processor->extractInfo(
|
|
$this->extensionPath,
|
|
[
|
|
'FooBarPlugins' => 'ext.baz.foobar',
|
|
] + self::$default,
|
|
1
|
|
);
|
|
}
|
|
|
|
public function testExtractPathBasedGlobal() {
|
|
$processor = new ExtensionProcessor();
|
|
$processor->extractInfo(
|
|
$this->extensionPath,
|
|
[
|
|
'ServiceWiringFiles' => [
|
|
'includes/ServiceWiring.php'
|
|
],
|
|
] + self::$default,
|
|
1
|
|
);
|
|
$globals = $processor->getExtractedInfo()['globals'];
|
|
$this->assertArrayHasKey( 'wgServiceWiringFiles', $globals );
|
|
$this->assertSame( [
|
|
"{$this->dirname}/includes/ServiceWiring.php"
|
|
], $globals['wgServiceWiringFiles'] );
|
|
}
|
|
|
|
public function testGetRequirements() {
|
|
$info = self::$default + [
|
|
'requires' => [
|
|
'MediaWiki' => '>= 1.25.0',
|
|
'platform' => [
|
|
'php' => '>= 5.5.9'
|
|
],
|
|
'extensions' => [
|
|
'Bar' => '*'
|
|
]
|
|
]
|
|
];
|
|
$processor = new ExtensionProcessor();
|
|
$this->assertSame(
|
|
$info['requires'],
|
|
$processor->getRequirements( $info, false )
|
|
);
|
|
$this->assertSame(
|
|
[],
|
|
$processor->getRequirements( [], false )
|
|
);
|
|
}
|
|
|
|
public function testGetDevRequirements() {
|
|
$info = self::$default + [
|
|
'dev-requires' => [
|
|
'MediaWiki' => '>= 1.31.0',
|
|
'platform' => [
|
|
'ext-foo' => '*',
|
|
],
|
|
'skins' => [
|
|
'Baz' => '*',
|
|
],
|
|
'extensions' => [
|
|
'Biz' => '*',
|
|
],
|
|
],
|
|
];
|
|
$processor = new ExtensionProcessor();
|
|
$this->assertSame(
|
|
$info['dev-requires'],
|
|
$processor->getRequirements( $info, true )
|
|
);
|
|
// Set some standard requirements, so we can test merging
|
|
$info['requires'] = [
|
|
'MediaWiki' => '>= 1.25.0',
|
|
'platform' => [
|
|
'php' => '>= 5.5.9'
|
|
],
|
|
'extensions' => [
|
|
'Bar' => '*'
|
|
]
|
|
];
|
|
$this->assertSame(
|
|
[
|
|
'MediaWiki' => '>= 1.25.0 >= 1.31.0',
|
|
'platform' => [
|
|
'php' => '>= 5.5.9',
|
|
'ext-foo' => '*',
|
|
],
|
|
'extensions' => [
|
|
'Bar' => '*',
|
|
'Biz' => '*',
|
|
],
|
|
'skins' => [
|
|
'Baz' => '*',
|
|
],
|
|
],
|
|
$processor->getRequirements( $info, true )
|
|
);
|
|
|
|
// If there's no dev-requires, it just returns requires
|
|
unset( $info['dev-requires'] );
|
|
$this->assertSame(
|
|
$info['requires'],
|
|
$processor->getRequirements( $info, true )
|
|
);
|
|
}
|
|
|
|
public function testGetExtraAutoloaderPaths() {
|
|
$this->hideDeprecated( 'ExtensionProcessor::getExtraAutoloaderPaths' );
|
|
$processor = new ExtensionProcessor();
|
|
$this->assertSame(
|
|
[ "{$this->dirname}/vendor/autoload.php" ],
|
|
$processor->getExtraAutoloaderPaths( $this->dirname, [
|
|
'load_composer_autoloader' => true,
|
|
] )
|
|
);
|
|
}
|
|
|
|
public function testGetExtractedAutoloadInfo() {
|
|
$processor = new ExtensionProcessor();
|
|
|
|
$processor->extractInfo( $this->extensionPath, [
|
|
'name' => 'Test',
|
|
'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
|
|
'AutoloadNamespaces' => [ '\Foo\Bar\\' => 'includes/foo/bar/' ],
|
|
'TestAutoloadClasses' => [ 'FooBarTest' => 'tests/FooBarTest.php' ],
|
|
'TestAutoloadNamespaces' => [ '\Foo\Bar\Test\\' => 'tests/foo/bar/' ],
|
|
'load_composer_autoloader' => true,
|
|
], 2 );
|
|
|
|
$autoload = $processor->getExtractedAutoloadInfo();
|
|
$this->assertSame(
|
|
[ 'FooBar' => $this->dirname . '/includes/FooBar.php' ],
|
|
$autoload['classes']
|
|
);
|
|
$this->assertSame(
|
|
[ '\Foo\Bar\\' => $this->dirname . '/includes/foo/bar/' ],
|
|
$autoload['namespaces']
|
|
);
|
|
$this->assertSame(
|
|
[ $this->dirname . '/vendor/autoload.php' ],
|
|
$autoload['files']
|
|
);
|
|
|
|
$autoload = $processor->getExtractedAutoloadInfo( true );
|
|
$this->assertSame(
|
|
[
|
|
'FooBar' => $this->dirname . '/includes/FooBar.php',
|
|
'FooBarTest' => $this->dirname . '/tests/FooBarTest.php'
|
|
],
|
|
$autoload['classes']
|
|
);
|
|
$this->assertSame(
|
|
[
|
|
'\Foo\Bar\\' => $this->dirname . '/includes/foo/bar/',
|
|
'\Foo\Bar\Test\\' => $this->dirname . '/tests/foo/bar/'
|
|
],
|
|
$autoload['namespaces']
|
|
);
|
|
$this->assertSame(
|
|
[ $this->dirname . '/vendor/autoload.php' ],
|
|
$autoload['files']
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Verify that extension.schema.json is in sync with ExtensionProcessor
|
|
*
|
|
* @coversNothing
|
|
*/
|
|
public function testGlobalSettingsDocumentedInSchema() {
|
|
global $IP;
|
|
$globalSettings = TestingAccessWrapper::newFromClass(
|
|
ExtensionProcessor::class )->globalSettings;
|
|
|
|
$version = ExtensionRegistry::MANIFEST_VERSION;
|
|
$schema = FormatJson::decode(
|
|
file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
|
|
true
|
|
);
|
|
$missing = [];
|
|
foreach ( $globalSettings as $global ) {
|
|
if ( !isset( $schema['properties'][$global] ) ) {
|
|
$missing[] = $global;
|
|
}
|
|
}
|
|
|
|
$this->assertEquals( [], $missing,
|
|
"The following global settings are not documented in docs/extension.schema.json" );
|
|
}
|
|
|
|
public function testGetCoreAttribsMerging() {
|
|
$processor = new ExtensionProcessor();
|
|
|
|
$info = self::$default + [
|
|
'TrackingCategories' => [
|
|
'Foo'
|
|
]
|
|
];
|
|
|
|
$info2 = self::$default2 + [
|
|
'TrackingCategories' => [
|
|
'Bar'
|
|
]
|
|
];
|
|
|
|
$processor->extractInfo( $this->extensionPath, $info, 2 );
|
|
$processor->extractInfo( $this->extensionPath, $info2, 2 );
|
|
|
|
$attributes = $processor->getExtractedInfo()['attributes'];
|
|
|
|
$this->assertEquals(
|
|
[ 'Foo', 'Bar' ],
|
|
$attributes['TrackingCategories']
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Allow overriding the default value of $this->globals and $this->attributes
|
|
* so we can test merging and hook extraction
|
|
*/
|
|
class MockExtensionProcessor extends ExtensionProcessor {
|
|
|
|
public function __construct( $preset = [] ) {
|
|
if ( isset( $preset['globals'] ) ) {
|
|
$this->globals = $preset['globals'] + $this->globals;
|
|
}
|
|
if ( isset( $preset['attributes'] ) ) {
|
|
$this->attributes = $preset['attributes'] + $this->attributes;
|
|
}
|
|
}
|
|
}
|