1<?php
2
3use Wikimedia\ScopedCallback;
4use Wikimedia\TestingAccessWrapper;
5
6/**
7 * @covers ExtensionRegistry
8 */
9class ExtensionRegistryTest extends MediaWikiIntegrationTestCase {
10
11	private $dataDir;
12
13	protected function setUp() : void {
14		parent::setUp();
15		$this->dataDir = __DIR__ . '/../../data/registration';
16	}
17
18	public function testQueue_invalid() {
19		$registry = new ExtensionRegistry();
20		$path = __DIR__ . '/doesnotexist.json';
21		$this->expectException( Exception::class );
22		$this->expectExceptionMessage( "file $path" );
23		$registry->queue( $path );
24	}
25
26	public function testQueue() {
27		$registry = new ExtensionRegistry();
28		$path = "{$this->dataDir}/good.json";
29		$registry->queue( $path );
30		$this->assertArrayHasKey(
31			$path,
32			$registry->getQueue()
33		);
34		$registry->clearQueue();
35		$this->assertSame( [], $registry->getQueue() );
36	}
37
38	public function testLoadFromQueue_empty() {
39		$registry = new ExtensionRegistry();
40		$registry->loadFromQueue();
41		$this->assertSame( [], $registry->getAllThings() );
42	}
43
44	public function testLoadFromQueue_late() {
45		$registry = new ExtensionRegistry();
46		$registry->finish();
47		$registry->queue( "{$this->dataDir}/good.json" );
48		$this->expectException( MWException::class );
49		$this->expectExceptionMessage(
50			"The following paths tried to load late: {$this->dataDir}/good.json" );
51		$registry->loadFromQueue();
52	}
53
54	public function testLoadFromQueue() {
55		$registry = new ExtensionRegistry();
56		$registry->queue( "{$this->dataDir}/good.json" );
57		$registry->loadFromQueue();
58		$this->assertArrayHasKey( 'FooBar', $registry->getAllThings() );
59		$this->assertTrue( $registry->isLoaded( 'FooBar' ) );
60		$this->assertTrue( $registry->isLoaded( 'FooBar', '*' ) );
61		$this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) );
62		$this->assertSame( [], $registry->getAttribute( 'NotLoadedAttr' ) );
63	}
64
65	public function testLoadFromQueueWithConstraintWithVersion() {
66		$registry = new ExtensionRegistry();
67		$registry->queue( "{$this->dataDir}/good_with_version.json" );
68		$registry->loadFromQueue();
69		$this->assertTrue( $registry->isLoaded( 'FooBar', '>= 1.2.0' ) );
70		$this->assertFalse( $registry->isLoaded( 'FooBar', '^1.3.0' ) );
71	}
72
73	public function testLoadFromQueueWithConstraintWithoutVersion() {
74		$registry = new ExtensionRegistry();
75		$registry->queue( "{$this->dataDir}/good.json" );
76		$registry->loadFromQueue();
77		$this->expectException( LogicException::class );
78		$registry->isLoaded( 'FooBar', '>= 1.2.0' );
79	}
80
81	public function testReadFromQueue_nonexistent() {
82		$registry = new ExtensionRegistry();
83		$this->expectError();
84		$registry->readFromQueue( [
85			__DIR__ . '/doesnotexist.json' => 1
86		] );
87	}
88
89	public function testReadFromQueueInitializeAutoloaderWithPsr4Namespaces() {
90		$registry = new ExtensionRegistry();
91		$registry->readFromQueue( [
92			"{$this->dataDir}/autoload_namespaces.json" => 1
93		] );
94		$this->assertTrue(
95			class_exists( 'Test\\MediaWiki\\AutoLoader\\TestFooBar' ),
96			"Registry initializes Autoloader from AutoloadNamespaces"
97		);
98	}
99
100	public function testExportExtractedDataNamespaceAlreadyDefined() {
101		define( 'FOO_VALUE', 123 ); // Emulates overriding a namespace set in LocalSettings.php
102		$registry = new ExtensionRegistry();
103		$info = [ 'defines' => [ 'FOO_VALUE' => 456 ], 'globals' => [] ];
104		$this->expectException( Exception::class );
105		$this->expectExceptionMessage(
106			"FOO_VALUE cannot be re-defined with 456 it has already been set with 123"
107		);
108		TestingAccessWrapper::newFromObject( $registry )->exportExtractedData( $info );
109	}
110
111	/**
112	 * @dataProvider provideExportExtractedDataGlobals
113	 */
114	public function testExportExtractedDataGlobals( $desc, $before, $globals, $expected ) {
115		// Set globals for test
116		if ( $before ) {
117			foreach ( $before as $key => $value ) {
118				// mw prefixed globals does not exist normally
119				if ( substr( $key, 0, 2 ) == 'mw' ) {
120					$GLOBALS[$key] = $value;
121				} else {
122					$this->setMwGlobals( $key, $value );
123				}
124			}
125		}
126
127		$info = [
128			'globals' => $globals,
129			'callbacks' => [],
130			'defines' => [],
131			'credits' => [],
132			'attributes' => [],
133			'autoloaderPaths' => []
134		];
135		$registry = new ExtensionRegistry();
136		TestingAccessWrapper::newFromObject( $registry )->exportExtractedData( $info );
137		foreach ( $expected as $name => $value ) {
138			$this->assertArrayHasKey( $name, $GLOBALS, $desc );
139			$this->assertEquals( $value, $GLOBALS[$name], $desc );
140		}
141
142		// Remove mw prefixed globals
143		if ( $before ) {
144			foreach ( $before as $key => $value ) {
145				if ( substr( $key, 0, 2 ) == 'mw' ) {
146					unset( $GLOBALS[$key] );
147				}
148			}
149		}
150	}
151
152	public static function provideExportExtractedDataGlobals() {
153		// "mwtest" prefix used instead of "$wg" to avoid potential conflicts
154		return [
155			[
156				'Simple non-array values',
157				[
158					'mwtestFooBarConfig' => true,
159					'mwtestFooBarConfig2' => 'string',
160				],
161				[
162					'mwtestFooBarDefault' => 1234,
163					'mwtestFooBarConfig' => false,
164				],
165				[
166					'mwtestFooBarConfig' => true,
167					'mwtestFooBarConfig2' => 'string',
168					'mwtestFooBarDefault' => 1234,
169				],
170			],
171			[
172				'No global already set, simple array',
173				null,
174				[
175					'mwtestDefaultOptions' => [
176						'foobar' => true,
177					]
178				],
179				[
180					'mwtestDefaultOptions' => [
181						'foobar' => true,
182					]
183				],
184			],
185			[
186				'Global already set, simple array',
187				[
188					'mwtestDefaultOptions' => [
189						'foobar' => true,
190						'foo' => 'string'
191					],
192				],
193				[
194					'mwtestDefaultOptions' => [
195						'barbaz' => 12345,
196						'foobar' => false,
197					],
198				],
199				[
200					'mwtestDefaultOptions' => [
201						'barbaz' => 12345,
202						'foo' => 'string',
203						'foobar' => true,
204					],
205				]
206			],
207			[
208				'Global already set, 1d array that appends',
209				[
210					'mwAvailableRights' => [
211						'foobar',
212						'foo'
213					],
214				],
215				[
216					'mwAvailableRights' => [
217						'barbaz',
218					],
219				],
220				[
221					'mwAvailableRights' => [
222						'barbaz',
223						'foobar',
224						'foo',
225					],
226				]
227			],
228			[
229				'Global already set, array with integer keys',
230				[
231					'mwNamespacesFoo' => [
232						100 => true,
233						102 => false
234					],
235				],
236				[
237					'mwNamespacesFoo' => [
238						100 => false,
239						500 => true,
240						ExtensionRegistry::MERGE_STRATEGY => 'array_plus',
241					],
242				],
243				[
244					'mwNamespacesFoo' => [
245						100 => true,
246						102 => false,
247						500 => true,
248					],
249				]
250			],
251			[
252				'No global already set, $wgHooks',
253				[
254					'wgHooks' => [],
255				],
256				[
257					'wgHooks' => [
258						'FooBarEvent' => [
259							'FooBarClass::onFooBarEvent'
260						],
261						ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive'
262					],
263				],
264				[
265					'wgHooks' => [
266						'FooBarEvent' => [
267							'FooBarClass::onFooBarEvent'
268						],
269					],
270				],
271			],
272			[
273				'Global already set, $wgHooks',
274				[
275					'wgHooks' => [
276						'FooBarEvent' => [
277							'FooBarClass::onFooBarEvent'
278						],
279						'BazBarEvent' => [
280							'FooBarClass::onBazBarEvent',
281						],
282					],
283				],
284				[
285					'wgHooks' => [
286						'FooBarEvent' => [
287							'BazBarClass::onFooBarEvent',
288						],
289						ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive',
290					],
291				],
292				[
293					'wgHooks' => [
294						'FooBarEvent' => [
295							'FooBarClass::onFooBarEvent',
296							'BazBarClass::onFooBarEvent',
297						],
298						'BazBarEvent' => [
299							'FooBarClass::onBazBarEvent',
300						],
301					],
302				],
303			],
304			[
305				'Global already set, $wgGroupPermissions',
306				[
307					'wgGroupPermissions' => [
308						'sysop' => [
309							'something' => true,
310						],
311						'user' => [
312							'somethingtwo' => true,
313						]
314					],
315				],
316				[
317					'wgGroupPermissions' => [
318						'customgroup' => [
319							'right' => true,
320						],
321						'user' => [
322							'right' => true,
323							'somethingtwo' => false,
324							'nonduplicated' => true,
325						],
326						ExtensionRegistry::MERGE_STRATEGY => 'array_plus_2d',
327					],
328				],
329				[
330					'wgGroupPermissions' => [
331						'customgroup' => [
332							'right' => true,
333						],
334						'sysop' => [
335							'something' => true,
336						],
337						'user' => [
338							'somethingtwo' => true,
339							'right' => true,
340							'nonduplicated' => true,
341						]
342					],
343				],
344			],
345			[
346				'False local setting should not be overridden (T100767)',
347				[
348					'mwtestT100767' => false,
349				],
350				[
351					'mwtestT100767' => true,
352				],
353				[
354					'mwtestT100767' => false,
355				],
356			],
357			[
358				'test array_replace_recursive',
359				[
360					'mwtestJsonConfigs' => [
361						'JsonZeroConfig' => [
362							'namespace' => 480,
363							'nsName' => 'Zero',
364							'isLocal' => false,
365							'remote' => [
366								'username' => 'foo',
367							],
368						],
369					],
370				],
371				[
372					'mwtestJsonConfigs' => [
373						'JsonZeroConfig' => [
374							'isLocal' => true,
375						],
376						ExtensionRegistry::MERGE_STRATEGY => 'array_replace_recursive',
377					],
378				],
379				[
380					'mwtestJsonConfigs' => [
381						'JsonZeroConfig' => [
382							'namespace' => 480,
383							'nsName' => 'Zero',
384							'isLocal' => false,
385							'remote' => [
386								'username' => 'foo',
387							],
388						],
389					],
390				],
391			],
392			[
393				'global is null before',
394				[
395					'NullGlobal' => null,
396				],
397				[
398					'NullGlobal' => 'not-null'
399				],
400				[
401					'NullGlobal' => null
402				],
403			],
404			[
405				'provide_default passive case',
406				[
407					'wgFlatArray' => [],
408				],
409				[
410					'wgFlatArray' => [
411						1,
412						ExtensionRegistry::MERGE_STRATEGY => 'provide_default'
413					],
414				],
415				[
416					'wgFlatArray' => []
417				],
418			],
419			[
420				'provide_default active case',
421				[
422				],
423				[
424					'wgFlatArray' => [
425						1,
426						ExtensionRegistry::MERGE_STRATEGY => 'provide_default'
427					],
428				],
429				[
430					'wgFlatArray' => [ 1 ]
431				],
432			]
433		];
434	}
435
436	public function testSetAttributeForTest() {
437		$registry = new ExtensionRegistry();
438		$registry->queue( "{$this->dataDir}/good.json" );
439		$registry->loadFromQueue();
440		// Sanity check that it worked
441		$this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) );
442		$reset = $registry->setAttributeForTest( 'FooBarAttr', [ 'override' ] );
443		// overridden properly
444		$this->assertSame( [ 'override' ], $registry->getAttribute( 'FooBarAttr' ) );
445		ScopedCallback::consume( $reset );
446		// reset properly
447		$this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) );
448	}
449
450	public function testSetAttributeForTestDuplicate() {
451		$registry = new ExtensionRegistry();
452		$reset1 = $registry->setAttributeForTest( 'foo', [ 'val1' ] );
453		$this->expectException( Exception::class );
454		$this->expectExceptionMessage( "The attribute 'foo' has already been overridden" );
455		$reset2 = $registry->setAttributeForTest( 'foo', [ 'val2' ] );
456	}
457
458	public function testGetLazyLoadedAttribute() {
459		$registry = TestingAccessWrapper::newFromObject(
460			new ExtensionRegistry()
461		);
462		// Verify the registry is absolutely empty
463		$this->assertSame( [], $registry->getLazyLoadedAttribute( 'FooBarBaz' ) );
464		// Indicate what paths should be checked for the lazy attributes
465		$registry->loaded = [
466			'FooBar' => [
467				'path' => "{$this->dataDir}/attribute.json",
468			]
469		];
470		// Set in attribute.json
471		$this->assertEquals(
472			[ 'buzz' => true ],
473			$registry->getLazyLoadedAttribute( 'FooBarBaz' )
474		);
475		// Still return an array if nothing was set
476		$this->assertSame(
477			[],
478			$registry->getLazyLoadedAttribute( 'NotSetAtAll' )
479		);
480
481		// Test test overrides
482		$reset = $registry->setAttributeForTest( 'FooBarBaz',
483			[ 'lightyear' => true ] );
484		$this->assertEquals(
485			[ 'lightyear' => true ],
486			$registry->getLazyLoadedAttribute( 'FooBarBaz' )
487		);
488	}
489}
490