4 * These tests should work regardless of $wgCapitalLinks
5 * @todo Split tests into providers and test methods
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
;
23 class LocalFileTest
extends MediaWikiIntegrationTestCase
{
24 use MockAuthorityTrait
;
26 private static function getDefaultInfo() {
29 'directory' => '/testdir',
32 'transformVia404' => false,
33 'backend' => new FSFileBackend( [
34 'name' => 'local-backend',
35 'wikiId' => WikiMap
::getCurrentWikiId(),
37 'cont1' => "/testdir/local-backend/tempimages/cont1",
38 'cont2' => "/testdir/local-backend/tempimages/cont2"
45 * @covers \File::getHashPath
46 * @dataProvider provideGetHashPath
47 * @param string $expected
48 * @param bool $capitalLinks
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() {
59 [ '', true, [ 'hashLevels' => 0 ] ],
60 [ 'a/a2/', true, [ 'hashLevels' => 2 ] ],
61 [ 'c/c4/', false, [ 'initialCapital' => false ] ],
66 * @covers \File::getRel
67 * @dataProvider provideGetRel
68 * @param string $expected
69 * @param bool $capitalLinks
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() {
81 [ 'Test!', true, [ 'hashLevels' => 0 ] ],
82 [ 'a/a2/Test!', true, [ 'hashLevels' => 2 ] ],
83 [ 'c/c4/test!', false, [ 'initialCapital' => false ] ],
88 * @covers \File::getUrlRel
89 * @dataProvider provideGetUrlRel
90 * @param string $expected
91 * @param bool $capitalLinks
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() {
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
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() {
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 ], [ '!' ]
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
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() {
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' ]
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
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() {
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
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() {
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
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() {
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
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() {
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
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() {
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
,
303 yield
'No permission, not deleted' => [
304 'performer' => $incapablePerformer,
305 'audience' => File
::FOR_THIS_USER
,
309 yield
'No permission, deleted' => [
310 'performer' => $incapablePerformer,
311 'audience' => File
::FOR_THIS_USER
,
312 'deleted' => File
::DELETED_USER | File
::DELETED_COMMENT
,
315 yield
'Not deleted, public' => [
316 'performer' => $capablePerformer,
317 'audience' => File
::FOR_PUBLIC
,
321 yield
'Deleted, public' => [
322 'performer' => $capablePerformer,
323 'audience' => File
::FOR_PUBLIC
,
324 'deleted' => File
::DELETED_USER | File
::DELETED_COMMENT
,
327 yield
'With permission, deleted' => [
328 'performer' => $capablePerformer,
329 'audience' => File
::FOR_THIS_USER
,
330 'deleted' => File
::DELETED_USER | File
::DELETED_COMMENT
,
335 private function getOldLocalFileWithDeletion(
336 UserIdentity
$uploader,
339 $this->getDb()->newInsertQueryBuilder()
340 ->insertInto( 'oldimage' )
342 'oi_name' => 'Random-11m.png',
343 'oi_archive_name' => 'Random-11m.png',
344 'oi_size' => 10816824,
349 'oi_media_type' => 'BITMAP',
350 'oi_major_mime' => 'image',
351 'oi_minor_mime' => 'png',
352 'oi_description_id' => $this->getServiceContainer()
354 ->createComment( $this->getDb(), 'comment' )->id
,
355 'oi_actor' => $this->getServiceContainer()
357 ->acquireActorId( $uploader, $this->getDb() ),
358 'oi_timestamp' => $this->getDb()->timestamp( '20201105235242' ),
359 'oi_sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru',
360 'oi_deleted' => $deletedFlags,
362 ->caller( __METHOD__
)
364 $file = OldLocalFile
::newFromTitle(
365 Title
::makeTitle( NS_FILE
, 'Random-11m.png' ),
366 $this->getServiceContainer()->getRepoGroup()->getLocalRepo(),
369 $this->assertInstanceOf( File
::class, $file, 'Created a test file' );
373 private function getArchivedFileWithDeletion(
374 UserIdentity
$uploader,
377 return ArchivedFile
::newFromRow( (object)[
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,
388 'fa_media_type' => 'BITMAP',
389 'fa_major_mime' => 'image',
390 'fa_minor_mime' => 'png',
391 'fa_description_id' => $this->getServiceContainer()
393 ->createComment( $this->getDb(), 'comment' )->id
,
394 'fa_actor' => $this->getServiceContainer()
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,
416 $file = $this->getOldLocalFileWithDeletion( $performer->getUser(), $deleted );
418 $this->assertTrue( $performer->getUser()->equals( $file->getUploader( $audience, $performer ) ) );
420 $this->assertNull( $file->getUploader( $audience, $performer ) );
425 * @dataProvider providePermissionChecks
426 * @covers \ArchivedFile::getDescription
428 public function testGetDescription(
429 Authority
$performer,
434 $file = $this->getArchivedFileWithDeletion( $performer->getUser(), $deleted );
436 $this->assertSame( 'comment', $file->getDescription( $audience, $performer ) );
438 $this->assertSame( '', $file->getDescription( $audience, $performer ) );
443 * @dataProvider providePermissionChecks
444 * @covers \ArchivedFile::getUploader
446 public function testArchivedGetUploader(
447 Authority
$performer,
452 $file = $this->getArchivedFileWithDeletion( $performer->getUser(), $deleted );
454 $this->assertTrue( $performer->getUser()->equals( $file->getUploader( $audience, $performer ) ) );
456 $this->assertNull( $file->getUploader( $audience, $performer ) );
461 * @dataProvider providePermissionChecks
462 * @covers \LocalFile::getDescription
464 public function testArchivedGetDescription(
465 Authority
$performer,
470 $file = $this->getOldLocalFileWithDeletion( $performer->getUser(), $deleted );
472 $this->assertSame( 'comment', $file->getDescription( $audience, $performer ) );
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(
504 $file = ( new LocalRepo( self
::getDefaultInfo() ) )->newFile( __METHOD__
);
505 $this->assertStringContainsString( 'TEST CONTENT', $file->getDescriptionText() );
508 public static function provideLoadFromDBAndCache() {
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;}}',
516 '{"data":{"frameCount":0,"loopCount":1,"duration":0,"bitDepth":16,"colorType":"truecolour","metadata":{"DateTime":"2019:07:30 13:52:32","_MW_PNG_VERSION":1}}}',
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}}}',
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}}}',
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 ) . '"' ],
539 * Test loadFromDB() and loadFromCache() and helpers
541 * @dataProvider provideLoadFromDBAndCache
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
;
554 'MainWANObjectCache',
555 new WANObjectCache( [
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' );
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 = [
581 'colorType' => 'truecolour',
583 'DateTime' => '2019:07:30 13:52:32',
584 '_MW_PNG_VERSION' => 1,
587 if ( $largeItemSize ) {
588 $expectedMetaArray['large'] = str_repeat( 'x', $largeItemSize );
591 'name' => 'Random-11m.png',
595 'metadata' => $expectedMetaArray,
597 'media_type' => 'BITMAP',
598 'mime' => 'image/png',
599 'timestamp' => '20201105235242',
600 'sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru'
603 $dbw->newInsertQueryBuilder()
604 ->insertInto( 'image' )
606 'img_name' => 'Random-11m.png',
607 'img_size' => 10816824,
609 'img_height' => 1800,
610 'img_metadata' => $dbw->encodeBlob( $meta ),
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__
)
622 $repo = $services->getRepoGroup()->getLocalRepo();
623 $file = $repo->findFile( $title );
625 $this->assertFileProperties( $expectedProps, $file );
626 $this->assertSame( 'truecolour', $file->getMetadataItem( 'colorType' ) );
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()
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' ) );
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(
672 'name', 'size', 'width', 'height',
673 'bits', 'media_type', 'mime', 'timestamp', 'sha1'
677 // Compare the other properties
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() {
702 * Test the legacy function LocalFile::getMetadata()
703 * @dataProvider provideLegacyMetadataRoundTrip
706 public function testLegacyMetadataRoundTrip( $meta ) {
707 $file = new class( $meta ) extends LocalFile
{
708 public function __construct( $meta ) {
709 $repo = MediaWikiServices
::getInstance()->getRepoGroup()->getLocalRepo();
711 Title
::makeTitle( NS_FILE
, 'TestLegacyMetadataRoundTrip' ),
713 $this->loadMetadataFromString( $meta );
714 $this->dataLoaded
= true;
717 $this->assertSame( $meta, $file->getMetadata() );
720 public static function provideRecordUpload3() {
727 'ImageDescription' => 'Test file',
728 'XResolution' => '72/1',
729 'YResolution' => '72/1',
730 'ResolutionUnit' => 2,
731 'YCbCrPositioning' => 1,
732 'JPEGFileComment' => [
735 'MEDIAWIKI_EXIF_VERSION' => 2,
737 'fileExists' => true,
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' => [
749 'fileExists' => true,
751 'file-mime' => 'application/pdf',
752 'major_mime' => 'application',
753 'minor_mime' => 'pdf',
754 'mime' => 'application/pdf',
755 'sha1' => '1o3l1yqjue2diq07grnnyq9kyapfpor',
757 'media_type' => 'OFFICE',
761 'Page 1 text .................................',
762 'Page 2 text .................................',
763 'Page 3 text .................................',
764 'Page 4 text .................................',
765 'Page 5 text .................................',
766 'Page 6 text .................................',
773 'fileExists' => true,
775 'file-mime' => 'application/pdf',
776 'major_mime' => 'application',
777 'minor_mime' => 'pdf',
778 'mime' => 'application/pdf',
779 'sha1' => '1o3l1yqjue2diq07grnnyq9kyapfpor',
781 'media_type' => 'OFFICE',
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() {
811 * Test recordUpload3() and confirm that file properties are reflected back
812 * after loading the new file from the DB.
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,
824 'backend' => new FSFileBackend( [
825 'name' => 'test-backend',
826 'wikiId' => WikiMap
::getCurrentWikiId(),
827 'basePath' => '/nonexistent'
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(
842 $this->getTestSysop()->getUser(),
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 );
856 public function testUpload() {
857 $repo = new LocalRepo(
859 'class' => LocalRepo
::class,
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(
878 $this->getTestUser()->getUser()
880 $this->assertStatusGood( $status );
883 $file = new LocalFile( $title, $repo );
884 $path = __DIR__
. '/../../../data/media/jpeg-xmp-nullchar.jpg';
885 $status = $file->upload(
892 $this->getTestUser()->getUser()
894 $this->assertStatusGood( $status );
897 public static function provideReserializeMetadata() {
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' )
932 'img_name' => 'Test.pdf',
936 'img_metadata' => $dbw->encodeBlob( $input ),
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__
)
949 $repo = new LocalRepo( [
950 'class' => LocalRepo
::class,
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'
963 $title = Title
::makeTitle( NS_FILE
, 'Test.pdf' );
964 $file = new LocalFile( $title, $repo );
965 TestingAccessWrapper
::newFromObject( $file )->handler
= $this->getMockPdfHandler();
967 $file->maybeUpgradeRow();
969 $metadata = $dbw->decodeBlob( $dbw->newSelectQueryBuilder()
970 ->select( 'img_metadata' )
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,
988 'updateCompatibleMetadata' => true,
989 'useJsonMetadata' => true,
991 'backend' => new FSFileBackend( [
992 'name' => 'test-backend',
993 'wikiId' => WikiMap
::getCurrentWikiId(),
994 'containerPaths' => [ 'test-public' => __DIR__
. '/../../../data/media' ]
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' )
1007 'img_name' => 'Png-native-test.png',
1011 'img_metadata' => $dbw->encodeBlob( 'a:1:{s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i: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__
)
1024 $title = Title
::makeTitle( NS_FILE
, 'Png-native-test.png' );
1025 $file = new LocalFile( $title, $repo );
1027 $file->maybeUpgradeRow();
1028 $metadata = $dbw->decodeBlob( $dbw->newSelectQueryBuilder()
1029 ->select( 'img_metadata' )
1031 ->where( [ 'img_name' => 'Png-native-test.png' ] )
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(
1043 'sha1' => '3n69qtiaif1swp3kyfueqjtmw2u4c2b',
1045 'media_type' => 'BITMAP',