1<?php
2
3use MediaWiki\MediaWikiServices;
4
5/**
6 * @coversDefaultClass LocalRepo
7 * @group Database
8 */
9class LocalRepoTest extends MediaWikiIntegrationTestCase {
10	/**
11	 * @param array $extraInfo To pass to LocalRepo constructor
12	 */
13	private function newRepo( array $extraInfo = [] ) {
14		return new LocalRepo( $extraInfo + [
15			'name' => 'local',
16			'backend' => 'local-backend',
17		] );
18	}
19
20	/**
21	 * @param array $extraInfo To pass to constructor
22	 * @param bool $expected
23	 * @dataProvider provideHasSha1Storage
24	 * @covers ::__construct
25	 */
26	public function testHasSha1Storage( array $extraInfo, $expected ) {
27		$this->assertSame( $expected, $this->newRepo( $extraInfo )->hasSha1Storage() );
28	}
29
30	public static function provideHasSha1Storage() {
31		return [
32			[ [], false ],
33			[ [ 'storageLayout' => 'sha256' ], false ],
34			[ [ 'storageLayout' => 'sha1' ], true ],
35		];
36	}
37
38	/**
39	 * @param string $prefix 'img' or 'oi'
40	 * @param string $expectedClass 'LocalFile' or 'OldLocalFile'
41	 * @dataProvider provideNewFileFromRow
42	 * @covers ::newFileFromRow
43	 */
44	public function testNewFileFromRow( $prefix, $expectedClass ) {
45		$this->editPage( 'File:Test_file', 'Some description' );
46
47		$row = (object)[
48			"{$prefix}_name" => 'Test_file',
49			// We cheat and include this for img_ too, it will be ignored
50			"{$prefix}_archive_name" => 'Archive_name',
51			"{$prefix}_user" => '1',
52			"{$prefix}_timestamp" => '12345678910111',
53			"{$prefix}_metadata" => '',
54			"{$prefix}_sha1" => sha1( '' ),
55			"{$prefix}_size" => '0',
56			"{$prefix}_height" => '0',
57			"{$prefix}_width" => '0',
58			"{$prefix}_bits" => '0',
59			"{$prefix}_description_text" => '',
60			"{$prefix}_description_data" => null,
61		];
62		$file = $this->newRepo()->newFileFromRow( $row );
63		$this->assertInstanceOf( $expectedClass, $file );
64		$this->assertSame( 'Test_file', $file->getName() );
65		$this->assertSame( 1, $file->getUser( 'id' ) );
66	}
67
68	public static function provideNewFileFromRow() {
69		return [
70			'img' => [ 'img', LocalFile::class ],
71			'oi' => [ 'oi', OldLocalFile::class ],
72		];
73	}
74
75	/**
76	 * @covers ::__construct
77	 * @covers ::newFileFromRow
78	 */
79	public function testNewFileFromRow_invalid() {
80		$this->expectException( MWException::class );
81		$this->expectExceptionMessage( 'LocalRepo::newFileFromRow: invalid row' );
82
83		$row = (object)[
84			"img_user" => '1',
85			"img_timestamp" => '12345678910111',
86			"img_metadata" => '',
87			"img_sha1" => sha1( '' ),
88			"img_size" => '0',
89			"img_height" => '0',
90			"img_width" => '0',
91			"img_bits" => '0',
92		];
93		$file = $this->newRepo()->newFileFromRow( $row );
94	}
95
96	/**
97	 * @covers ::__construct
98	 * @covers ::newFromArchiveName
99	 */
100	public function testNewFromArchiveName() {
101		$this->editPage( 'File:Test_file', 'Some description' );
102
103		$file = $this->newRepo()->newFromArchiveName( 'Test_file', 'b' );
104		$this->assertInstanceOf( OldLocalFile::class, $file );
105		$this->assertSame( 'Test_file', $file->getName() );
106	}
107
108	// TODO cleanupDeletedBatch, deletedFileHasKey, hiddenFileHasKey
109
110	/**
111	 * @covers ::__construct
112	 * @covers ::cleanupDeletedBatch
113	 */
114	public function testCleanupDeletedBatch_sha1Storage() {
115		$this->assertEquals( Status::newGood(),
116			$this->newRepo( [ 'storageLayout' => 'sha1' ] )->cleanupDeletedBatch( [] ) );
117	}
118
119	/**
120	 * @param string $input
121	 * @param string $expected
122	 * @dataProvider provideGetHashFromKey
123	 * @covers ::getHashFromKey
124	 */
125	public function testGetHashFromKey( $input, $expected ) {
126		$this->assertSame( $expected, LocalRepo::getHashFromKey( $input ) );
127	}
128
129	public static function provideGetHashFromKey() {
130		return [
131			[ '', false ],
132			[ '.', false ],
133			[ 'a.', 'a' ],
134			[ '.b', 'b' ],
135			[ '..c', 'c' ],
136			[ 'd.x', 'd' ],
137			[ '.e.x', 'e' ],
138			[ '..f.x', 'f' ],
139			[ 'g..x', 'g' ],
140			[ '01234567890123456789012345678901.x', '1234567890123456789012345678901' ],
141		];
142	}
143
144	/**
145	 * @covers ::__construct
146	 * @covers ::checkRedirect
147	 */
148	public function testCheckRedirect_nonRedirect() {
149		$this->editPage( 'File:Not a redirect', 'Not a redirect' );
150		$this->assertFalse(
151			$this->newRepo()->checkRedirect( Title::makeTitle( NS_FILE, 'Not a redirect' ) ) );
152	}
153
154	/**
155	 * @covers ::__construct
156	 * @covers ::checkRedirect
157	 * @covers ::getSharedCacheKey
158	 */
159	public function testCheckRedirect_redirect() {
160		$this->editPage( 'File:Redirect', '#REDIRECT [[File:Target]]' );
161		$this->assertEquals( 'File:Target',
162			$this->newRepo()->checkRedirect( Title::makeTitle( NS_FILE, 'Redirect' ) )
163				->getPrefixedText() );
164	}
165
166	/**
167	 * @covers ::__construct
168	 * @covers ::checkRedirect
169	 * @covers ::getSharedCacheKey
170	 * @covers ::getLocalCacheKey
171	 */
172	public function testCheckRedirect_redirect_noWANCache() {
173		$this->markTestIncomplete( 'WANObjectCache::makeKey is final' );
174
175		$mockWan = $this->getMockBuilder( WANObjectCache::class )
176			->setConstructorArgs( [ [ 'cache' => new EmptyBagOStuff ] ] )
177			->setMethods( [ 'makeKey' ] )
178			->getMock();
179		$mockWan->expects( $this->exactly( 2 ) )->method( 'makeKey' )->withConsecutive(
180			[ 'file_redirect', md5( 'Redirect' ) ],
181			[ 'filerepo', 'local', 'file_redirect', md5( 'Redirect' ) ]
182		)->will( $this->onConsecutiveCalls( false, 'somekey' ) );
183
184		$repo = $this->newRepo( [ 'wanCache' => $mockWan ] );
185
186		$this->editPage( 'File:Redirect', '#REDIRECT [[File:Target]]' );
187		$this->assertEquals( 'File:Target',
188			$repo->checkRedirect( Title::makeTitle( NS_FILE, 'Redirect' ) )->getPrefixedText() );
189	}
190
191	/**
192	 * @covers ::__construct
193	 * @covers ::checkRedirect
194	 */
195	public function testCheckRedirect_invalidFile() {
196		$this->expectException( MWException::class );
197		$this->expectExceptionMessage( '`Notafile` is not a valid file title.' );
198		$this->newRepo()->checkRedirect( Title::makeTitle( NS_MAIN, 'Notafile' ) );
199	}
200
201	/**
202	 * @covers ::__construct
203	 * @covers ::findBySha1
204	 */
205	public function testFindBySha1() {
206		$this->markTestIncomplete( "Haven't figured out how to upload files yet" );
207
208		$repo = $this->newRepo();
209
210		$tmpFileFactory = MediaWikiServices::getInstance()->getTempFSFileFactory();
211		foreach ( [ 'File1', 'File2', 'File3' ] as $name ) {
212			$fsFile = $tmpFileFactory->newTempFSFile( '' );
213			file_put_contents( $fsFile->getPath(), "$name contents" );
214			$localFile = $repo->newFile( $name );
215			$localFile->upload( $fsFile, 'Uploaded', "$name desc" );
216		}
217	}
218
219	/**
220	 * @covers ::__construct
221	 * @covers ::getSharedCacheKey
222	 * @covers ::checkRedirect
223	 * @covers ::invalidateImageRedirect
224	 */
225	public function testInvalidateImageRedirect() {
226		global $wgTestMe;
227		$wgTestMe = true;
228		$repo = $this->newRepo(
229			[ 'wanCache' => new WANObjectCache( [ 'cache' => new HashBagOStuff ] ) ] );
230
231		$title = Title::makeTitle( NS_FILE, 'Redirect' );
232
233		$this->editPage( 'File:Redirect', '#REDIRECT [[File:Target]]' );
234
235		$this->assertSame( 'File:Target',
236			$repo->checkRedirect( $title )->getPrefixedText() );
237
238		$this->editPage( 'File:Redirect', 'No longer a redirect' );
239
240		$this->assertSame( 'File:Target',
241			$repo->checkRedirect( $title )->getPrefixedText() );
242
243		$repo->invalidateImageRedirect( $title );
244
245		$this->markTestIncomplete(
246			"Can't figure out how to get image redirect validation to take effect" );
247
248		$this->assertSame( false, $repo->checkRedirect( $title ) );
249	}
250
251	/**
252	 * @covers ::getInfo
253	 */
254	public function testGetInfo() {
255		$this->setMwGlobals( [
256			'wgFavicon' => '//example.com/favicon.ico',
257			'wgSitename' => 'Test my site',
258		] );
259
260		$repo = $this->newRepo( [ 'favicon' => 'Hey, this option is ignored in LocalRepo!' ] );
261
262		$this->assertSame( [
263			'name' => 'local',
264			'displayname' => 'Test my site',
265			'rootUrl' => false,
266			'local' => true,
267			'url' => false,
268			'thumbUrl' => false,
269			'initialCapital' => true,
270			// XXX This assumes protocol-relative will get expanded to http instead of https
271			'favicon' => 'http://example.com/favicon.ico',
272		], $repo->getInfo() );
273	}
274
275	// XXX The following getInfo tests are really testing FileRepo, not LocalRepo, but we want to
276	// make sure they're true for LocalRepo too. How should we do this? A trait?
277
278	/**
279	 * @covers ::getInfo
280	 */
281	public function testGetInfo_name() {
282		$this->assertSame( 'some-name',
283			$this->newRepo( [ 'name' => 'some-name' ] )->getInfo()['name'] );
284	}
285
286	/**
287	 * @covers ::getInfo
288	 */
289	public function testGetInfo_displayName() {
290		$this->assertSame( wfMessage( 'shared-repo' )->text(),
291			$this->newRepo( [ 'name' => 'not-local' ] )->getInfo()['displayname'] );
292	}
293
294	/**
295	 * @covers ::getInfo
296	 */
297	public function testGetInfo_displayNameCustomMsg() {
298		$this->editPage( 'MediaWiki:Shared-repo-name-not-local', 'Name to display please' );
299		// Allow the message to take effect
300		MediaWikiServices::getInstance()->getMessageCache()->enable();
301
302		$this->assertSame( 'Name to display please',
303			$this->newRepo( [ 'name' => 'not-local' ] )->getInfo()['displayname'] );
304	}
305
306	/**
307	 * @covers ::getInfo
308	 */
309	public function testGetInfo_rootUrl() {
310		$this->assertSame( 'https://my.url',
311			$this->newRepo( [ 'url' => 'https://my.url' ] )->getInfo()['rootUrl'] );
312	}
313
314	/**
315	 * @covers ::getInfo
316	 */
317	public function testGetInfo_rootUrlCustomized() {
318		$this->assertSame(
319			'https://my.url/some/sub/dir',
320			$this->newRepo( [
321				'url' => 'https://my.url',
322				'zones' => [ 'public' => [ 'url' => 'https://my.url/some/sub/dir' ] ],
323			] )->getInfo()['rootUrl']
324		);
325	}
326
327	/**
328	 * @covers ::getInfo
329	 */
330	public function testGetInfo_local() {
331		$this->assertFalse( $this->newRepo( [ 'name' => 'not-local' ] )->getInfo()['local'] );
332	}
333
334	/**
335	 * @param string $setting
336	 * @dataProvider provideGetInfo_optionalSettings
337	 * @covers ::getInfo
338	 */
339	public function testGetInfo_optionalSettings( $setting ) {
340		$this->assertSame( 'dummy test value',
341			$this->newRepo( [ $setting => 'dummy test value' ] )->getInfo()[$setting] );
342	}
343
344	public static function provideGetInfo_optionalSettings() {
345		return [
346			[ 'url' ],
347			[ 'thumbUrl' ],
348			[ 'initialCapital' ],
349			[ 'descBaseUrl' ],
350			[ 'scriptDirUrl' ],
351			[ 'articleUrl' ],
352			[ 'fetchDescription' ],
353			[ 'descriptionCacheExpiry' ],
354		];
355	}
356
357	/**
358	 * @param string $method
359	 * @param mixed ...$args
360	 * @dataProvider provideSkipWriteOperationIfSha1
361	 * @covers ::store
362	 * @covers ::storeBatch
363	 * @covers ::cleanupBatch
364	 * @covers ::publish
365	 * @covers ::publishBatch
366	 * @covers ::delete
367	 * @covers ::deleteBatch
368	 * @covers ::skipWriteOperationIfSha1
369	 */
370	public function testSkipWriteOperationIfSha1( $method, ...$args ) {
371		$repo = $this->newRepo( [ 'storageLayout' => 'sha1' ] );
372		$this->assertEquals( Status::newGood(), $repo->$method( ...$args ) );
373	}
374
375	public static function provideSkipWriteOperationIfSha1() {
376		return [
377			[ 'store', '', '', '' ],
378			[ 'storeBatch', [ '' ] ],
379			[ 'cleanupBatch', [ '' ] ],
380			[ 'publish', '', '', '' ],
381			[ 'publishBatch', [ '' ] ],
382			[ 'delete', '', '' ],
383			[ 'deleteBatch', [ '' ] ],
384		];
385	}
386}
387