Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / filerepo / file / LocalFileTest.php
blob064766e8a1bfa5b6889cc1e8b064e803fe871078
1 <?php
3 /**
4 * These tests should work regardless of $wgCapitalLinks
5 * @todo Split tests into providers and test methods
6 */
8 use MediaWiki\MainConfigNames;
9 use MediaWiki\MediaWikiServices;
10 use MediaWiki\Permissions\Authority;
11 use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
12 use MediaWiki\Title\Title;
13 use MediaWiki\User\UserIdentity;
14 use MediaWiki\WikiMap\WikiMap;
15 use Wikimedia\FileBackend\FSFileBackend;
16 use Wikimedia\ObjectCache\HashBagOStuff;
17 use Wikimedia\ObjectCache\WANObjectCache;
18 use Wikimedia\TestingAccessWrapper;
20 /**
21 * @group Database
23 class LocalFileTest extends MediaWikiIntegrationTestCase {
24 use MockAuthorityTrait;
26 private static function getDefaultInfo() {
27 return [
28 'name' => 'test',
29 'directory' => '/testdir',
30 'url' => '/testurl',
31 'hashLevels' => 2,
32 'transformVia404' => false,
33 'backend' => new FSFileBackend( [
34 'name' => 'local-backend',
35 'wikiId' => WikiMap::getCurrentWikiId(),
36 'containerPaths' => [
37 'cont1' => "/testdir/local-backend/tempimages/cont1",
38 'cont2' => "/testdir/local-backend/tempimages/cont2"
40 ] )
44 /**
45 * @covers \File::getHashPath
46 * @dataProvider provideGetHashPath
47 * @param string $expected
48 * @param bool $capitalLinks
49 * @param array $info
51 public function testGetHashPath( $expected, $capitalLinks, array $info ) {
52 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
53 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
54 ->newFile( 'test!' )->getHashPath() );
57 public static function provideGetHashPath() {
58 return [
59 [ '', true, [ 'hashLevels' => 0 ] ],
60 [ 'a/a2/', true, [ 'hashLevels' => 2 ] ],
61 [ 'c/c4/', false, [ 'initialCapital' => false ] ],
65 /**
66 * @covers \File::getRel
67 * @dataProvider provideGetRel
68 * @param string $expected
69 * @param bool $capitalLinks
70 * @param array $info
72 public function testGetRel( $expected, $capitalLinks, array $info ) {
73 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
75 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
76 ->newFile( 'test!' )->getRel() );
79 public static function provideGetRel() {
80 return [
81 [ 'Test!', true, [ 'hashLevels' => 0 ] ],
82 [ 'a/a2/Test!', true, [ 'hashLevels' => 2 ] ],
83 [ 'c/c4/test!', false, [ 'initialCapital' => false ] ],
87 /**
88 * @covers \File::getUrlRel
89 * @dataProvider provideGetUrlRel
90 * @param string $expected
91 * @param bool $capitalLinks
92 * @param array $info
94 public function testGetUrlRel( $expected, $capitalLinks, array $info ) {
95 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
97 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
98 ->newFile( 'test!' )->getUrlRel() );
101 public static function provideGetUrlRel() {
102 return [
103 [ 'Test%21', true, [ 'hashLevels' => 0 ] ],
104 [ 'a/a2/Test%21', true, [ 'hashLevels' => 2 ] ],
105 [ 'c/c4/test%21', false, [ 'initialCapital' => false ] ],
110 * @covers \File::getArchivePath
111 * @dataProvider provideGetArchivePath
112 * @param string $expected
113 * @param bool $capitalLinks
114 * @param array $info
115 * @param array $args
117 public function testGetArchivePath( $expected, $capitalLinks, array $info, array $args ) {
118 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
120 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
121 ->newFile( 'test!' )->getArchivePath( ...$args ) );
124 public static function provideGetArchivePath() {
125 return [
126 [ 'mwstore://local-backend/test-public/archive', true, [ 'hashLevels' => 0 ], [] ],
127 [ 'mwstore://local-backend/test-public/archive/a/a2', true, [ 'hashLevels' => 2 ], [] ],
129 'mwstore://local-backend/test-public/archive/!',
130 true, [ 'hashLevels' => 0 ], [ '!' ]
131 ], [
132 'mwstore://local-backend/test-public/archive/a/a2/!',
133 true, [ 'hashLevels' => 2 ], [ '!' ]
139 * @covers \File::getThumbPath
140 * @dataProvider provideGetThumbPath
141 * @param string $expected
142 * @param bool $capitalLinks
143 * @param array $info
144 * @param array $args
146 public function testGetThumbPath( $expected, $capitalLinks, array $info, array $args ) {
147 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
149 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
150 ->newFile( 'test!' )->getThumbPath( ...$args ) );
153 public static function provideGetThumbPath() {
154 return [
155 [ 'mwstore://local-backend/test-thumb/Test!', true, [ 'hashLevels' => 0 ], [] ],
156 [ 'mwstore://local-backend/test-thumb/a/a2/Test!', true, [ 'hashLevels' => 2 ], [] ],
158 'mwstore://local-backend/test-thumb/Test!/x',
159 true, [ 'hashLevels' => 0 ], [ 'x' ]
160 ], [
161 'mwstore://local-backend/test-thumb/a/a2/Test!/x',
162 true, [ 'hashLevels' => 2 ], [ 'x' ]
168 * @covers \File::getArchiveUrl
169 * @dataProvider provideGetArchiveUrl
170 * @param string $expected
171 * @param bool $capitalLinks
172 * @param array $info
173 * @param array $args
175 public function testGetArchiveUrl( $expected, $capitalLinks, array $info, array $args ) {
176 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
178 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
179 ->newFile( 'test!' )->getArchiveUrl( ...$args ) );
182 public static function provideGetArchiveUrl() {
183 return [
184 [ '/testurl/archive', true, [ 'hashLevels' => 0 ], [] ],
185 [ '/testurl/archive/a/a2', true, [ 'hashLevels' => 2 ], [] ],
186 [ '/testurl/archive/%21', true, [ 'hashLevels' => 0 ], [ '!' ] ],
187 [ '/testurl/archive/a/a2/%21', true, [ 'hashLevels' => 2 ], [ '!' ] ],
192 * @covers \File::getThumbUrl
193 * @dataProvider provideGetThumbUrl
194 * @param string $expected
195 * @param bool $capitalLinks
196 * @param array $info
197 * @param array $args
199 public function testGetThumbUrl( $expected, $capitalLinks, array $info, array $args ) {
200 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
202 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
203 ->newFile( 'test!' )->getThumbUrl( ...$args ) );
206 public static function provideGetThumbUrl() {
207 return [
208 [ '/testurl/thumb/Test%21', true, [ 'hashLevels' => 0 ], [] ],
209 [ '/testurl/thumb/a/a2/Test%21', true, [ 'hashLevels' => 2 ], [] ],
210 [ '/testurl/thumb/Test%21/x', true, [ 'hashLevels' => 0 ], [ 'x' ] ],
211 [ '/testurl/thumb/a/a2/Test%21/x', true, [ 'hashLevels' => 2 ], [ 'x' ] ],
216 * @covers \File::getArchiveVirtualUrl
217 * @dataProvider provideGetArchiveVirtualUrl
218 * @param string $expected
219 * @param bool $capitalLinks
220 * @param array $info
221 * @param array $args
223 public function testGetArchiveVirtualUrl(
224 $expected, $capitalLinks, array $info, array $args
226 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
228 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
229 ->newFile( 'test!' )->getArchiveVirtualUrl( ...$args ) );
232 public static function provideGetArchiveVirtualUrl() {
233 return [
234 [ 'mwrepo://test/public/archive', true, [ 'hashLevels' => 0 ], [] ],
235 [ 'mwrepo://test/public/archive/a/a2', true, [ 'hashLevels' => 2 ], [] ],
236 [ 'mwrepo://test/public/archive/%21', true, [ 'hashLevels' => 0 ], [ '!' ] ],
237 [ 'mwrepo://test/public/archive/a/a2/%21', true, [ 'hashLevels' => 2 ], [ '!' ] ],
242 * @covers \File::getThumbVirtualUrl
243 * @dataProvider provideGetThumbVirtualUrl
244 * @param string $expected
245 * @param bool $capitalLinks
246 * @param array $info
247 * @param array $args
249 public function testGetThumbVirtualUrl( $expected, $capitalLinks, array $info, array $args ) {
250 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
252 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
253 ->newFile( 'test!' )->getThumbVirtualUrl( ...$args ) );
256 public static function provideGetThumbVirtualUrl() {
257 return [
258 [ 'mwrepo://test/thumb/Test%21', true, [ 'hashLevels' => 0 ], [] ],
259 [ 'mwrepo://test/thumb/a/a2/Test%21', true, [ 'hashLevels' => 2 ], [] ],
260 [ 'mwrepo://test/thumb/Test%21/%21', true, [ 'hashLevels' => 0 ], [ '!' ] ],
261 [ 'mwrepo://test/thumb/a/a2/Test%21/%21', true, [ 'hashLevels' => 2 ], [ '!' ] ],
266 * @covers \File::getUrl
267 * @dataProvider provideGetUrl
268 * @param string $expected
269 * @param bool $capitalLinks
270 * @param array $info
272 public function testGetUrl( $expected, $capitalLinks, array $info ) {
273 $this->overrideConfigValue( MainConfigNames::CapitalLinks, $capitalLinks );
275 $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) )
276 ->newFile( 'test!' )->getUrl() );
279 public static function provideGetUrl() {
280 return [
281 [ '/testurl/Test%21', true, [ 'hashLevels' => 0 ] ],
282 [ '/testurl/a/a2/Test%21', true, [ 'hashLevels' => 2 ] ],
287 * @covers \LocalFile::getUploader
289 public function testGetUploaderForNonExistingFile() {
290 $file = ( new LocalRepo( self::getDefaultInfo() ) )->newFile( 'test!' );
291 $this->assertNull( $file->getUploader() );
294 public function providePermissionChecks() {
295 $capablePerformer = $this->mockRegisteredAuthorityWithPermissions( [ 'deletedhistory', 'deletedtext' ] );
296 $incapablePerformer = $this->mockRegisteredAuthorityWithoutPermissions( [ 'deletedhistory', 'deletedtext' ] );
297 yield 'Deleted, RAW' => [
298 'performer' => $incapablePerformer,
299 'audience' => File::RAW,
300 'deleted' => File::DELETED_USER | File::DELETED_COMMENT,
301 'expected' => true,
303 yield 'No permission, not deleted' => [
304 'performer' => $incapablePerformer,
305 'audience' => File::FOR_THIS_USER,
306 'deleted' => 0,
307 'expected' => true,
309 yield 'No permission, deleted' => [
310 'performer' => $incapablePerformer,
311 'audience' => File::FOR_THIS_USER,
312 'deleted' => File::DELETED_USER | File::DELETED_COMMENT,
313 'expected' => false,
315 yield 'Not deleted, public' => [
316 'performer' => $capablePerformer,
317 'audience' => File::FOR_PUBLIC,
318 'deleted' => 0,
319 'expected' => true,
321 yield 'Deleted, public' => [
322 'performer' => $capablePerformer,
323 'audience' => File::FOR_PUBLIC,
324 'deleted' => File::DELETED_USER | File::DELETED_COMMENT,
325 'expected' => false,
327 yield 'With permission, deleted' => [
328 'performer' => $capablePerformer,
329 'audience' => File::FOR_THIS_USER,
330 'deleted' => File::DELETED_USER | File::DELETED_COMMENT,
331 'expected' => true,
335 private function getOldLocalFileWithDeletion(
336 UserIdentity $uploader,
337 int $deletedFlags
338 ): OldLocalFile {
339 $this->getDb()->newInsertQueryBuilder()
340 ->insertInto( 'oldimage' )
341 ->row( [
342 'oi_name' => 'Random-11m.png',
343 'oi_archive_name' => 'Random-11m.png',
344 'oi_size' => 10816824,
345 'oi_width' => 1000,
346 'oi_height' => 1800,
347 'oi_metadata' => '',
348 'oi_bits' => 16,
349 'oi_media_type' => 'BITMAP',
350 'oi_major_mime' => 'image',
351 'oi_minor_mime' => 'png',
352 'oi_description_id' => $this->getServiceContainer()
353 ->getCommentStore()
354 ->createComment( $this->getDb(), 'comment' )->id,
355 'oi_actor' => $this->getServiceContainer()
356 ->getActorStore()
357 ->acquireActorId( $uploader, $this->getDb() ),
358 'oi_timestamp' => $this->getDb()->timestamp( '20201105235242' ),
359 'oi_sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru',
360 'oi_deleted' => $deletedFlags,
362 ->caller( __METHOD__ )
363 ->execute();
364 $file = OldLocalFile::newFromTitle(
365 Title::makeTitle( NS_FILE, 'Random-11m.png' ),
366 $this->getServiceContainer()->getRepoGroup()->getLocalRepo(),
367 '20201105235242'
369 $this->assertInstanceOf( File::class, $file, 'Created a test file' );
370 return $file;
373 private function getArchivedFileWithDeletion(
374 UserIdentity $uploader,
375 int $deletedFlags
376 ): ArchivedFile {
377 return ArchivedFile::newFromRow( (object)[
378 'fa_id' => 1,
379 'fa_storage_group' => 'test',
380 'fa_storage_key' => 'bla',
381 'fa_name' => 'Random-11m.png',
382 'fa_archive_name' => 'Random-11m.png',
383 'fa_size' => 10816824,
384 'fa_width' => 1000,
385 'fa_height' => 1800,
386 'fa_metadata' => '',
387 'fa_bits' => 16,
388 'fa_media_type' => 'BITMAP',
389 'fa_major_mime' => 'image',
390 'fa_minor_mime' => 'png',
391 'fa_description_id' => $this->getServiceContainer()
392 ->getCommentStore()
393 ->createComment( $this->getDb(), 'comment' )->id,
394 'fa_actor' => $this->getServiceContainer()
395 ->getActorStore()
396 ->acquireActorId( $uploader, $this->getDb() ),
397 'fa_user' => $uploader->getId(),
398 'fa_user_text' => $uploader->getName(),
399 'fa_timestamp' => $this->getDb()->timestamp( '20201105235242' ),
400 'fa_sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru',
401 'fa_deleted' => $deletedFlags,
407 * @dataProvider providePermissionChecks
408 * @covers \LocalFile::getUploader
410 public function testGetUploader(
411 Authority $performer,
412 int $audience,
413 int $deleted,
414 bool $expected
416 $file = $this->getOldLocalFileWithDeletion( $performer->getUser(), $deleted );
417 if ( $expected ) {
418 $this->assertTrue( $performer->getUser()->equals( $file->getUploader( $audience, $performer ) ) );
419 } else {
420 $this->assertNull( $file->getUploader( $audience, $performer ) );
425 * @dataProvider providePermissionChecks
426 * @covers \ArchivedFile::getDescription
428 public function testGetDescription(
429 Authority $performer,
430 int $audience,
431 int $deleted,
432 bool $expected
434 $file = $this->getArchivedFileWithDeletion( $performer->getUser(), $deleted );
435 if ( $expected ) {
436 $this->assertSame( 'comment', $file->getDescription( $audience, $performer ) );
437 } else {
438 $this->assertSame( '', $file->getDescription( $audience, $performer ) );
443 * @dataProvider providePermissionChecks
444 * @covers \ArchivedFile::getUploader
446 public function testArchivedGetUploader(
447 Authority $performer,
448 int $audience,
449 int $deleted,
450 bool $expected
452 $file = $this->getArchivedFileWithDeletion( $performer->getUser(), $deleted );
453 if ( $expected ) {
454 $this->assertTrue( $performer->getUser()->equals( $file->getUploader( $audience, $performer ) ) );
455 } else {
456 $this->assertNull( $file->getUploader( $audience, $performer ) );
461 * @dataProvider providePermissionChecks
462 * @covers \LocalFile::getDescription
464 public function testArchivedGetDescription(
465 Authority $performer,
466 int $audience,
467 int $deleted,
468 bool $expected
470 $file = $this->getOldLocalFileWithDeletion( $performer->getUser(), $deleted );
471 if ( $expected ) {
472 $this->assertSame( 'comment', $file->getDescription( $audience, $performer ) );
473 } else {
474 $this->assertSame( '', $file->getDescription( $audience, $performer ) );
479 * @covers \File::getDescriptionShortUrl
481 public function testDescriptionShortUrlForNonExistingFile() {
482 $file = ( new LocalRepo( self::getDefaultInfo() ) )->newFile( 'test!' );
483 $this->assertNull( $file->getDescriptionShortUrl() );
487 * @covers \LocalFile::getDescriptionText
489 public function testDescriptionText_NonExisting() {
490 $file = ( new LocalRepo( self::getDefaultInfo() ) )->newFile( 'test!' );
491 $this->assertFalse( $file->getDescriptionText() );
495 * @covers \LocalFile::getDescriptionText
497 public function testDescriptionText_Existing() {
498 $this->assertTrue( $this->editPage(
499 __METHOD__,
500 'TEST CONTENT',
502 NS_FILE
503 )->isOK() );
504 $file = ( new LocalRepo( self::getDefaultInfo() ) )->newFile( __METHOD__ );
505 $this->assertStringContainsString( 'TEST CONTENT', $file->getDescriptionText() );
508 public static function provideLoadFromDBAndCache() {
509 return [
510 'legacy' => [
511 'a:6:{s:10:"frameCount";i:0;s:9:"loopCount";i:1;s:8:"duration";d:0;s:8:"bitDepth";i:16;s:9:"colorType";s:10:"truecolour";s:8:"metadata";a:2:{s:8:"DateTime";s:19:"2019:07:30 13:52:32";s:15:"_MW_PNG_VERSION";i:1;}}',
513 false,
515 'json' => [
516 '{"data":{"frameCount":0,"loopCount":1,"duration":0,"bitDepth":16,"colorType":"truecolour","metadata":{"DateTime":"2019:07:30 13:52:32","_MW_PNG_VERSION":1}}}',
518 false,
520 'json with blobs' => [
521 '{"blobs":{"colorType":"__BLOB0__"},"data":{"frameCount":0,"loopCount":1,"duration":0,"bitDepth":16,"metadata":{"DateTime":"2019:07:30 13:52:32","_MW_PNG_VERSION":1}}}',
522 [ '"truecolour"' ],
523 false,
525 'large (>100KB triggers uncached case)' => [
526 '{"data":{"large":"' . str_repeat( 'x', 102401 ) . '","frameCount":0,"loopCount":1,"duration":0,"bitDepth":16,"colorType":"truecolour","metadata":{"DateTime":"2019:07:30 13:52:32","_MW_PNG_VERSION":1}}}',
528 102401,
530 'large json blob' => [
531 '{"blobs":{"large":"__BLOB0__"},"data":{"frameCount":0,"loopCount":1,"duration":0,"bitDepth":16,"colorType":"truecolour","metadata":{"DateTime":"2019:07:30 13:52:32","_MW_PNG_VERSION":1}}}',
532 [ '"' . str_repeat( 'x', 102401 ) . '"' ],
533 102401,
539 * Test loadFromDB() and loadFromCache() and helpers
541 * @dataProvider provideLoadFromDBAndCache
542 * @covers \File
543 * @covers \LocalFile
544 * @param string $meta
545 * @param array $blobs Metadata blob values
546 * @param int|false $largeItemSize The size of the "large" metadata item,
547 * or false if there will be no such item.
549 public function testLoadFromDBAndCache( $meta, $blobs, $largeItemSize ) {
550 $services = $this->getServiceContainer();
552 $cache = new HashBagOStuff;
553 $this->setService(
554 'MainWANObjectCache',
555 new WANObjectCache( [
556 'cache' => $cache
560 $dbw = $this->getDb();
561 $norm = $services->getActorNormalization();
562 $user = $this->getTestSysop()->getUserIdentity();
563 $actorId = $norm->acquireActorId( $user, $dbw );
564 $comment = $services->getCommentStore()->createComment( $dbw, 'comment' );
565 $title = Title::makeTitle( NS_FILE, 'Random-11m.png' );
567 if ( $blobs ) {
568 $blobStore = $services->getBlobStore();
569 foreach ( $blobs as $i => $value ) {
570 $address = $blobStore->storeBlob( $value );
571 $meta = str_replace( "__BLOB{$i}__", $address, $meta );
575 // The provided metadata strings should all unserialize to this
576 $expectedMetaArray = [
577 'frameCount' => 0,
578 'loopCount' => 1,
579 'duration' => 0.0,
580 'bitDepth' => 16,
581 'colorType' => 'truecolour',
582 'metadata' => [
583 'DateTime' => '2019:07:30 13:52:32',
584 '_MW_PNG_VERSION' => 1,
587 if ( $largeItemSize ) {
588 $expectedMetaArray['large'] = str_repeat( 'x', $largeItemSize );
590 $expectedProps = [
591 'name' => 'Random-11m.png',
592 'size' => 10816824,
593 'width' => 1000,
594 'height' => 1800,
595 'metadata' => $expectedMetaArray,
596 'bits' => 16,
597 'media_type' => 'BITMAP',
598 'mime' => 'image/png',
599 'timestamp' => '20201105235242',
600 'sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru'
603 $dbw->newInsertQueryBuilder()
604 ->insertInto( 'image' )
605 ->row( [
606 'img_name' => 'Random-11m.png',
607 'img_size' => 10816824,
608 'img_width' => 1000,
609 'img_height' => 1800,
610 'img_metadata' => $dbw->encodeBlob( $meta ),
611 'img_bits' => 16,
612 'img_media_type' => 'BITMAP',
613 'img_major_mime' => 'image',
614 'img_minor_mime' => 'png',
615 'img_description_id' => $comment->id,
616 'img_actor' => $actorId,
617 'img_timestamp' => $dbw->timestamp( '20201105235242' ),
618 'img_sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru',
620 ->caller( __METHOD__ )
621 ->execute();
622 $repo = $services->getRepoGroup()->getLocalRepo();
623 $file = $repo->findFile( $title );
625 $this->assertFileProperties( $expectedProps, $file );
626 $this->assertSame( 'truecolour', $file->getMetadataItem( 'colorType' ) );
627 $this->assertSame(
628 [ 'loopCount' => 1, 'bitDepth' => 16 ],
629 $file->getMetadataItems( [ 'loopCount', 'bitDepth', 'nonexistent' ] )
631 $this->assertSame( 'comment', $file->getDescription() );
632 $this->assertTrue( $user->equals( $file->getUploader() ) );
634 // Test cache by corrupting DB
635 // Don't wipe img_metadata though since that will be loaded by loadExtraFromDB()
636 $dbw->newUpdateQueryBuilder()
637 ->update( 'image' )
638 ->set( [ 'img_size' => 0 ] )
639 ->where( [ 'img_name' => 'Random-11m.png' ] )
640 ->caller( __METHOD__ )->execute();
641 $file = LocalFile::newFromTitle( $title, $repo );
643 $this->assertFileProperties( $expectedProps, $file );
644 $this->assertSame( 'truecolour', $file->getMetadataItem( 'colorType' ) );
645 $this->assertSame(
646 [ 'loopCount' => 1, 'bitDepth' => 16 ],
647 $file->getMetadataItems( [ 'loopCount', 'bitDepth', 'nonexistent' ] )
649 $this->assertSame( 'comment', $file->getDescription() );
650 $this->assertTrue( $user->equals( $file->getUploader() ) );
652 // Make sure we were actually hitting the WAN cache
653 $dbw->newDeleteQueryBuilder()
654 ->deleteFrom( 'image' )
655 ->where( [ 'img_name' => 'Random-11m.png' ] )
656 ->caller( __METHOD__ )->execute();
657 $file->invalidateCache();
658 $file = LocalFile::newFromTitle( $title, $repo );
659 $this->assertSame( false, $file->exists() );
662 private function assertFileProperties( $expectedProps, $file ) {
663 // Compare metadata without ordering
664 if ( isset( $expectedProps['metadata'] ) ) {
665 $this->assertArrayEquals( $expectedProps['metadata'], $file->getMetadataArray() );
668 // Filter out unsupported expected properties
669 $expectedProps = array_intersect_key(
670 $expectedProps,
671 array_fill_keys( [
672 'name', 'size', 'width', 'height',
673 'bits', 'media_type', 'mime', 'timestamp', 'sha1'
674 ], true )
677 // Compare the other properties
678 $actualProps = [
679 'name' => $file->getName(),
680 'size' => $file->getSize(),
681 'width' => $file->getWidth(),
682 'height' => $file->getHeight(),
683 'bits' => $file->getBitDepth(),
684 'media_type' => $file->getMediaType(),
685 'mime' => $file->getMimeType(),
686 'timestamp' => $file->getTimestamp(),
687 'sha1' => $file->getSha1()
689 $actualProps = array_intersect_key( $actualProps, $expectedProps );
690 $this->assertArrayEquals( $expectedProps, $actualProps, false, true );
693 public static function provideLegacyMetadataRoundTrip() {
694 return [
695 [ '0' ],
696 [ '-1' ],
697 [ '' ]
702 * Test the legacy function LocalFile::getMetadata()
703 * @dataProvider provideLegacyMetadataRoundTrip
704 * @covers \LocalFile
706 public function testLegacyMetadataRoundTrip( $meta ) {
707 $file = new class( $meta ) extends LocalFile {
708 public function __construct( $meta ) {
709 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
710 parent::__construct(
711 Title::makeTitle( NS_FILE, 'TestLegacyMetadataRoundTrip' ),
712 $repo );
713 $this->loadMetadataFromString( $meta );
714 $this->dataLoaded = true;
717 $this->assertSame( $meta, $file->getMetadata() );
720 public static function provideRecordUpload3() {
721 $files = [
722 'test.jpg' => [
723 'width' => 20,
724 'height' => 20,
725 'bits' => 8,
726 'metadata' => [
727 'ImageDescription' => 'Test file',
728 'XResolution' => '72/1',
729 'YResolution' => '72/1',
730 'ResolutionUnit' => 2,
731 'YCbCrPositioning' => 1,
732 'JPEGFileComment' => [
733 'Created with GIMP',
735 'MEDIAWIKI_EXIF_VERSION' => 2,
737 'fileExists' => true,
738 'size' => 437,
739 'file-mime' => 'image/jpeg',
740 'major_mime' => 'image',
741 'minor_mime' => 'jpeg',
742 'mime' => 'image/jpeg',
743 'sha1' => '620ezvucfyia1mltnavzpqg9gmai2gf',
744 'media_type' => 'BITMAP',
746 'large-text.pdf' => [
747 'width' => 1275,
748 'height' => 1650,
749 'fileExists' => true,
750 'size' => 10598657,
751 'file-mime' => 'application/pdf',
752 'major_mime' => 'application',
753 'minor_mime' => 'pdf',
754 'mime' => 'application/pdf',
755 'sha1' => '1o3l1yqjue2diq07grnnyq9kyapfpor',
756 'bits' => 0,
757 'media_type' => 'OFFICE',
758 'metadata' => [
759 'Pages' => '6',
760 'text' => [
761 'Page 1 text .................................',
762 'Page 2 text .................................',
763 'Page 3 text .................................',
764 'Page 4 text .................................',
765 'Page 5 text .................................',
766 'Page 6 text .................................',
770 'no-text.pdf' => [
771 'width' => 1275,
772 'height' => 1650,
773 'fileExists' => true,
774 'size' => 10598657,
775 'file-mime' => 'application/pdf',
776 'major_mime' => 'application',
777 'minor_mime' => 'pdf',
778 'mime' => 'application/pdf',
779 'sha1' => '1o3l1yqjue2diq07grnnyq9kyapfpor',
780 'bits' => 0,
781 'media_type' => 'OFFICE',
782 'metadata' => [
783 'Pages' => '6',
787 $configurations = [
789 [ 'useJsonMetadata' => true ],
791 'useJsonMetadata' => true,
792 'useSplitMetadata' => true,
793 'splitMetadataThreshold' => 50
796 return ArrayUtils::cartesianProduct( $files, $configurations );
799 private function getMockPdfHandler() {
800 return new class extends ImageHandler {
801 public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) {
804 public function useSplitMetadata() {
805 return true;
811 * Test recordUpload3() and confirm that file properties are reflected back
812 * after loading the new file from the DB.
814 * @covers \LocalFile
815 * @dataProvider provideRecordUpload3
816 * @param array $props File properties
817 * @param array $conf LocalRepo configuration overrides
819 public function testRecordUpload3( $props, $conf ) {
820 $repo = new LocalRepo(
822 'class' => LocalRepo::class,
823 'name' => 'test',
824 'backend' => new FSFileBackend( [
825 'name' => 'test-backend',
826 'wikiId' => WikiMap::getCurrentWikiId(),
827 'basePath' => '/nonexistent'
829 ] + $conf
831 $title = Title::makeTitle( NS_FILE, 'Test.jpg' );
832 $file = new LocalFile( $title, $repo );
834 if ( $props['mime'] === 'application/pdf' ) {
835 TestingAccessWrapper::newFromObject( $file )->handler = $this->getMockPdfHandler();
838 $status = $file->recordUpload3(
839 'oldver',
840 'comment',
841 'page text',
842 $this->getTestSysop()->getUser(),
843 $props
845 $this->assertStatusGood( $status );
846 // Check properties of the same object immediately after upload
847 $this->assertFileProperties( $props, $file );
848 // Check round-trip through the DB
849 $file = new LocalFile( $title, $repo );
850 $this->assertFileProperties( $props, $file );
854 * @covers \LocalFile
856 public function testUpload() {
857 $repo = new LocalRepo(
859 'class' => LocalRepo::class,
860 'name' => 'test',
861 'backend' => new FSFileBackend( [
862 'name' => 'test-backend',
863 'wikiId' => WikiMap::getCurrentWikiId(),
864 'basePath' => $this->getNewTempDirectory()
868 $title = Title::makeTitle( NS_FILE, 'Test.jpg' );
869 $file = new LocalFile( $title, $repo );
870 $path = __DIR__ . '/../../../data/media/test.jpg';
871 $status = $file->upload(
872 $path,
873 'comment',
874 'page text',
876 false,
877 false,
878 $this->getTestUser()->getUser()
880 $this->assertStatusGood( $status );
882 // Test reupload
883 $file = new LocalFile( $title, $repo );
884 $path = __DIR__ . '/../../../data/media/jpeg-xmp-nullchar.jpg';
885 $status = $file->upload(
886 $path,
887 'comment',
888 'page text',
890 false,
891 false,
892 $this->getTestUser()->getUser()
894 $this->assertStatusGood( $status );
897 public static function provideReserializeMetadata() {
898 return [
904 'a:1:{s:4:"test";i:1;}',
905 '{"data":{"test":1}}'
908 serialize( [ 'test' => str_repeat( 'x', 100 ) ] ),
909 '{"data":[],"blobs":{"test":"tt:%d"}}'
915 * Test reserializeMetadata() via maybeUpgradeRow()
917 * @covers \LocalFile::maybeUpgradeRow
918 * @covers \LocalFile::reserializeMetadata
919 * @dataProvider provideReserializeMetadata
921 public function testReserializeMetadata( $input, $expected ) {
922 $dbw = $this->getDb();
923 $services = $this->getServiceContainer();
924 $norm = $services->getActorNormalization();
925 $user = $this->getTestSysop()->getUserIdentity();
926 $actorId = $norm->acquireActorId( $user, $dbw );
927 $comment = $services->getCommentStore()->createComment( $dbw, 'comment' );
929 $dbw->newInsertQueryBuilder()
930 ->insertInto( 'image' )
931 ->row( [
932 'img_name' => 'Test.pdf',
933 'img_size' => 1,
934 'img_width' => 1,
935 'img_height' => 1,
936 'img_metadata' => $dbw->encodeBlob( $input ),
937 'img_bits' => 0,
938 'img_media_type' => 'OFFICE',
939 'img_major_mime' => 'application',
940 'img_minor_mime' => 'pdf',
941 'img_description_id' => $comment->id,
942 'img_actor' => $actorId,
943 'img_timestamp' => $dbw->timestamp( '20201105235242' ),
944 'img_sha1' => 'hhhh',
946 ->caller( __METHOD__ )
947 ->execute();
949 $repo = new LocalRepo( [
950 'class' => LocalRepo::class,
951 'name' => 'test',
952 'useJsonMetadata' => true,
953 'useSplitMetadata' => true,
954 'splitMetadataThreshold' => 50,
955 'updateCompatibleMetadata' => true,
956 'reserializeMetadata' => true,
957 'backend' => new FSFileBackend( [
958 'name' => 'test-backend',
959 'wikiId' => WikiMap::getCurrentWikiId(),
960 'basePath' => '/nonexistent'
962 ] );
963 $title = Title::makeTitle( NS_FILE, 'Test.pdf' );
964 $file = new LocalFile( $title, $repo );
965 TestingAccessWrapper::newFromObject( $file )->handler = $this->getMockPdfHandler();
966 $file->load();
967 $file->maybeUpgradeRow();
969 $metadata = $dbw->decodeBlob( $dbw->newSelectQueryBuilder()
970 ->select( 'img_metadata' )
971 ->from( 'image' )
972 ->where( [ 'img_name' => 'Test.pdf' ] )
973 ->caller( __METHOD__ )->fetchField()
975 $this->assertStringMatchesFormat( $expected, $metadata );
979 * Test upgradeRow() via maybeUpgradeRow()
981 * @covers \LocalFile::maybeUpgradeRow
982 * @covers \LocalFile::upgradeRow
984 public function testUpgradeRow() {
985 $repo = new LocalRepo( [
986 'class' => LocalRepo::class,
987 'name' => 'test',
988 'updateCompatibleMetadata' => true,
989 'useJsonMetadata' => true,
990 'hashLevels' => 0,
991 'backend' => new FSFileBackend( [
992 'name' => 'test-backend',
993 'wikiId' => WikiMap::getCurrentWikiId(),
994 'containerPaths' => [ 'test-public' => __DIR__ . '/../../../data/media' ]
996 ] );
997 $dbw = $this->getDb();
998 $services = $this->getServiceContainer();
999 $norm = $services->getActorNormalization();
1000 $user = $this->getTestSysop()->getUserIdentity();
1001 $actorId = $norm->acquireActorId( $user, $dbw );
1002 $comment = $services->getCommentStore()->createComment( $dbw, 'comment' );
1004 $dbw->newInsertQueryBuilder()
1005 ->insertInto( 'image' )
1006 ->row( [
1007 'img_name' => 'Png-native-test.png',
1008 'img_size' => 1,
1009 'img_width' => 1,
1010 'img_height' => 1,
1011 'img_metadata' => $dbw->encodeBlob( 'a:1:{s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:0;}}' ),
1012 'img_bits' => 0,
1013 'img_media_type' => 'OFFICE',
1014 'img_major_mime' => 'image',
1015 'img_minor_mime' => 'png',
1016 'img_description_id' => $comment->id,
1017 'img_actor' => $actorId,
1018 'img_timestamp' => $dbw->timestamp( '20201105235242' ),
1019 'img_sha1' => 'hhhh',
1021 ->caller( __METHOD__ )
1022 ->execute();
1024 $title = Title::makeTitle( NS_FILE, 'Png-native-test.png' );
1025 $file = new LocalFile( $title, $repo );
1026 $file->load();
1027 $file->maybeUpgradeRow();
1028 $metadata = $dbw->decodeBlob( $dbw->newSelectQueryBuilder()
1029 ->select( 'img_metadata' )
1030 ->from( 'image' )
1031 ->where( [ 'img_name' => 'Png-native-test.png' ] )
1032 ->fetchField()
1034 // Just confirm that it looks like JSON with real metadata
1035 $this->assertStringStartsWith( '{"data":{"frameCount":0,', $metadata );
1037 $file = new LocalFile( $title, $repo );
1038 $this->assertFileProperties(
1040 'size' => 4665,
1041 'width' => 420,
1042 'height' => 300,
1043 'sha1' => '3n69qtiaif1swp3kyfueqjtmw2u4c2b',
1044 'bits' => 8,
1045 'media_type' => 'BITMAP',
1047 $file );