tablesUsed[] = 'image'; $this->tablesUsed[] = 'oldimage'; $this->tablesUsed[] = 'page'; $this->tablesUsed[] = 'text'; } private static function getDefaultInfo() { return [ 'name' => 'test', 'directory' => '/testdir', 'url' => '/testurl', 'hashLevels' => 2, 'transformVia404' => false, 'backend' => new FSFileBackend( [ 'name' => 'local-backend', 'wikiId' => wfWikiID(), 'containerPaths' => [ 'cont1' => "/testdir/local-backend/tempimages/cont1", 'cont2' => "/testdir/local-backend/tempimages/cont2" ] ] ) ]; } /** * @covers File::getHashPath * @dataProvider provideGetHashPath * @param string $expected * @param bool $capitalLinks * @param array $info */ public function testGetHashPath( $expected, $capitalLinks, array $info ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getHashPath() ); } public static function provideGetHashPath() { return [ [ '', true, [ 'hashLevels' => 0 ] ], [ 'a/a2/', true, [ 'hashLevels' => 2 ] ], [ 'c/c4/', false, [ 'initialCapital' => false ] ], ]; } /** * @covers File::getRel * @dataProvider provideGetRel * @param string $expected * @param bool $capitalLinks * @param array $info */ public function testGetRel( $expected, $capitalLinks, array $info ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getRel() ); } public static function provideGetRel() { return [ [ 'Test!', true, [ 'hashLevels' => 0 ] ], [ 'a/a2/Test!', true, [ 'hashLevels' => 2 ] ], [ 'c/c4/test!', false, [ 'initialCapital' => false ] ], ]; } /** * @covers File::getUrlRel * @dataProvider provideGetUrlRel * @param string $expected * @param bool $capitalLinks * @param array $info */ public function testGetUrlRel( $expected, $capitalLinks, array $info ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getUrlRel() ); } public static function provideGetUrlRel() { return [ [ 'Test%21', true, [ 'hashLevels' => 0 ] ], [ 'a/a2/Test%21', true, [ 'hashLevels' => 2 ] ], [ 'c/c4/test%21', false, [ 'initialCapital' => false ] ], ]; } /** * @covers File::getArchivePath * @dataProvider provideGetArchivePath * @param string $expected * @param bool $capitalLinks * @param array $info * @param array $args */ public function testGetArchivePath( $expected, $capitalLinks, array $info, array $args ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getArchivePath( ...$args ) ); } public static function provideGetArchivePath() { return [ [ 'mwstore://local-backend/test-public/archive', true, [ 'hashLevels' => 0 ], [] ], [ 'mwstore://local-backend/test-public/archive/a/a2', true, [ 'hashLevels' => 2 ], [] ], [ 'mwstore://local-backend/test-public/archive/!', true, [ 'hashLevels' => 0 ], [ '!' ] ], [ 'mwstore://local-backend/test-public/archive/a/a2/!', true, [ 'hashLevels' => 2 ], [ '!' ] ], ]; } /** * @covers File::getThumbPath * @dataProvider provideGetThumbPath * @param string $expected * @param bool $capitalLinks * @param array $info * @param array $args */ public function testGetThumbPath( $expected, $capitalLinks, array $info, array $args ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getThumbPath( ...$args ) ); } public static function provideGetThumbPath() { return [ [ 'mwstore://local-backend/test-thumb/Test!', true, [ 'hashLevels' => 0 ], [] ], [ 'mwstore://local-backend/test-thumb/a/a2/Test!', true, [ 'hashLevels' => 2 ], [] ], [ 'mwstore://local-backend/test-thumb/Test!/x', true, [ 'hashLevels' => 0 ], [ 'x' ] ], [ 'mwstore://local-backend/test-thumb/a/a2/Test!/x', true, [ 'hashLevels' => 2 ], [ 'x' ] ], ]; } /** * @covers File::getArchiveUrl * @dataProvider provideGetArchiveUrl * @param string $expected * @param bool $capitalLinks * @param array $info * @param array $args */ public function testGetArchiveUrl( $expected, $capitalLinks, array $info, array $args ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getArchiveUrl( ...$args ) ); } public static function provideGetArchiveUrl() { return [ [ '/testurl/archive', true, [ 'hashLevels' => 0 ], [] ], [ '/testurl/archive/a/a2', true, [ 'hashLevels' => 2 ], [] ], [ '/testurl/archive/%21', true, [ 'hashLevels' => 0 ], [ '!' ] ], [ '/testurl/archive/a/a2/%21', true, [ 'hashLevels' => 2 ], [ '!' ] ], ]; } /** * @covers File::getThumbUrl * @dataProvider provideGetThumbUrl * @param string $expected * @param bool $capitalLinks * @param array $info * @param array $args */ public function testGetThumbUrl( $expected, $capitalLinks, array $info, array $args ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getThumbUrl( ...$args ) ); } public static function provideGetThumbUrl() { return [ [ '/testurl/thumb/Test%21', true, [ 'hashLevels' => 0 ], [] ], [ '/testurl/thumb/a/a2/Test%21', true, [ 'hashLevels' => 2 ], [] ], [ '/testurl/thumb/Test%21/x', true, [ 'hashLevels' => 0 ], [ 'x' ] ], [ '/testurl/thumb/a/a2/Test%21/x', true, [ 'hashLevels' => 2 ], [ 'x' ] ], ]; } /** * @covers File::getArchiveVirtualUrl * @dataProvider provideGetArchiveVirtualUrl * @param string $expected * @param bool $capitalLinks * @param array $info * @param array $args */ public function testGetArchiveVirtualUrl( $expected, $capitalLinks, array $info, array $args ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getArchiveVirtualUrl( ...$args ) ); } public static function provideGetArchiveVirtualUrl() { return [ [ 'mwrepo://test/public/archive', true, [ 'hashLevels' => 0 ], [] ], [ 'mwrepo://test/public/archive/a/a2', true, [ 'hashLevels' => 2 ], [] ], [ 'mwrepo://test/public/archive/%21', true, [ 'hashLevels' => 0 ], [ '!' ] ], [ 'mwrepo://test/public/archive/a/a2/%21', true, [ 'hashLevels' => 2 ], [ '!' ] ], ]; } /** * @covers File::getThumbVirtualUrl * @dataProvider provideGetThumbVirtualUrl * @param string $expected * @param bool $capitalLinks * @param array $info * @param array $args */ public function testGetThumbVirtualUrl( $expected, $capitalLinks, array $info, array $args ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getThumbVirtualUrl( ...$args ) ); } public static function provideGetThumbVirtualUrl() { return [ [ 'mwrepo://test/thumb/Test%21', true, [ 'hashLevels' => 0 ], [] ], [ 'mwrepo://test/thumb/a/a2/Test%21', true, [ 'hashLevels' => 2 ], [] ], [ 'mwrepo://test/thumb/Test%21/%21', true, [ 'hashLevels' => 0 ], [ '!' ] ], [ 'mwrepo://test/thumb/a/a2/Test%21/%21', true, [ 'hashLevels' => 2 ], [ '!' ] ], ]; } /** * @covers File::getUrl * @dataProvider provideGetUrl * @param string $expected * @param bool $capitalLinks * @param array $info */ public function testGetUrl( $expected, $capitalLinks, array $info ) { $this->setMwGlobals( 'wgCapitalLinks', $capitalLinks ); $this->assertSame( $expected, ( new LocalRepo( $info + self::getDefaultInfo() ) ) ->newFile( 'test!' )->getUrl() ); } public static function provideGetUrl() { return [ [ '/testurl/Test%21', true, [ 'hashLevels' => 0 ] ], [ '/testurl/a/a2/Test%21', true, [ 'hashLevels' => 2 ] ], ]; } /** * @covers ::wfLocalFile */ public function testWfLocalFile() { $this->hideDeprecated( 'wfLocalFile' ); $file = wfLocalFile( "File:Some_file_that_probably_doesn't exist.png" ); $this->assertInstanceOf( LocalFile::class, $file, 'wfLocalFile() returns LocalFile for valid Titles' ); } /** * @covers LocalFile::getUploader */ public function testGetUploaderForNonExistingFile() { $file = ( new LocalRepo( self::getDefaultInfo() ) )->newFile( 'test!' ); $this->assertNull( $file->getUploader() ); } public function providePermissionChecks() { $capablePerformer = $this->mockAnonAuthorityWithPermissions( [ 'deletedhistory', 'deletedtext' ] ); $incapablePerformer = $this->mockAnonAuthorityWithoutPermissions( [ 'deletedhistory', 'deletedtext' ] ); yield 'Deleted, RAW' => [ 'performer' => $incapablePerformer, 'audience' => File::RAW, 'deleted' => File::DELETED_USER | File::DELETED_COMMENT, 'expected' => true, ]; yield 'No permission, not deleted' => [ 'performer' => $incapablePerformer, 'audience' => File::FOR_THIS_USER, 'deleted' => 0, 'expected' => true, ]; yield 'No permission, deleted' => [ 'performer' => $incapablePerformer, 'audience' => File::FOR_THIS_USER, 'deleted' => File::DELETED_USER | File::DELETED_COMMENT, 'expected' => false, ]; yield 'Not deleted, public' => [ 'performer' => $capablePerformer, 'audience' => File::FOR_PUBLIC, 'deleted' => 0, 'expected' => true, ]; yield 'Deleted, public' => [ 'performer' => $capablePerformer, 'audience' => File::FOR_PUBLIC, 'deleted' => File::DELETED_USER | File::DELETED_COMMENT, 'expected' => false, ]; yield 'With permission, deleted' => [ 'performer' => $capablePerformer, 'audience' => File::FOR_THIS_USER, 'deleted' => File::DELETED_USER | File::DELETED_COMMENT, 'expected' => true, ]; } private function getOldLocalFileWithDeletion( UserIdentity $uploader, int $deletedFlags ): OldLocalFile { $this->db->insert( 'oldimage', [ 'oi_name' => 'Random-11m.png', 'oi_archive_name' => 'Random-11m.png', 'oi_size' => 10816824, 'oi_width' => 1000, 'oi_height' => 1800, 'oi_metadata' => '', 'oi_bits' => 16, 'oi_media_type' => 'BITMAP', 'oi_major_mime' => 'image', 'oi_minor_mime' => 'png', 'oi_description_id' => $this->getServiceContainer() ->getCommentStore() ->createComment( $this->db, 'comment' )->id, 'oi_actor' => $this->getServiceContainer() ->getActorStore() ->acquireActorId( $uploader, $this->db ), 'oi_timestamp' => $this->db->timestamp( '20201105235242' ), 'oi_sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru', 'oi_deleted' => $deletedFlags, ] ); $file = OldLocalFile::newFromTitle( Title::newFromText( 'File:Random-11m.png' ), $this->getServiceContainer()->getRepoGroup()->getLocalRepo(), '20201105235242' ); $this->assertInstanceOf( File::class, $file, 'Sanity: created a test file' ); return $file; } private function getArchivedFileWithDeletion( UserIdentity $uploader, int $deletedFlags ): ArchivedFile { return ArchivedFile::newFromRow( (object)[ 'fa_id' => 1, 'fa_storage_group' => 'test', 'fa_storage_key' => 'bla', 'fa_name' => 'Random-11m.png', 'fa_archive_name' => 'Random-11m.png', 'fa_size' => 10816824, 'fa_width' => 1000, 'fa_height' => 1800, 'fa_metadata' => '', 'fa_bits' => 16, 'fa_media_type' => 'BITMAP', 'fa_major_mime' => 'image', 'fa_minor_mime' => 'png', 'fa_description_id' => $this->getServiceContainer() ->getCommentStore() ->createComment( $this->db, 'comment' )->id, 'fa_actor' => $this->getServiceContainer() ->getActorStore() ->acquireActorId( $uploader, $this->db ), 'fa_user' => $uploader->getId(), 'fa_user_text' => $uploader->getName(), 'fa_timestamp' => $this->db->timestamp( '20201105235242' ), 'fa_sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru', 'fa_deleted' => $deletedFlags, ] ); } /** * @dataProvider providePermissionChecks * @covers LocalFile::getUploader */ public function testGetUploader( Authority $performer, int $audience, int $deleted, bool $expected ) { $file = $this->getOldLocalFileWithDeletion( $performer->getUser(), $deleted ); if ( $expected ) { $this->assertTrue( $performer->getUser()->equals( $file->getUploader( $audience, $performer ) ) ); } else { $this->assertNull( $file->getUploader( $audience, $performer ) ); } } /** * @dataProvider providePermissionChecks * @covers ArchivedFile::getDescription */ public function testGetDescription( Authority $performer, int $audience, int $deleted, bool $expected ) { $file = $this->getArchivedFileWithDeletion( $performer->getUser(), $deleted ); if ( $expected ) { $this->assertSame( 'comment', $file->getDescription( $audience, $performer ) ); } else { $this->assertSame( '', $file->getDescription( $audience, $performer ) ); } } /** * @dataProvider providePermissionChecks * @covers ArchivedFile::getUploader */ public function testArchivedGetUploader( Authority $performer, int $audience, int $deleted, bool $expected ) { $file = $this->getArchivedFileWithDeletion( $performer->getUser(), $deleted ); if ( $expected ) { $this->assertTrue( $performer->getUser()->equals( $file->getUploader( $audience, $performer ) ) ); } else { $this->assertNull( $file->getUploader( $audience, $performer ) ); } } /** * @dataProvider providePermissionChecks * @covers LocalFile::getDescription */ public function testArchivedGetDescription( Authority $performer, int $audience, int $deleted, bool $expected ) { $file = $this->getOldLocalFileWithDeletion( $performer->getUser(), $deleted ); if ( $expected ) { $this->assertSame( 'comment', $file->getDescription( $audience, $performer ) ); } else { $this->assertSame( '', $file->getDescription( $audience, $performer ) ); } } /** * @covers File::getDescriptionShortUrl */ public function testDescriptionShortUrlForNonExistingFile() { $file = ( new LocalRepo( self::getDefaultInfo() ) )->newFile( 'test!' ); $this->assertNull( $file->getDescriptionShortUrl() ); } /** * @covers File::getDescriptionText */ public function testDescriptionTextForNonExistingFile() { $file = ( new LocalRepo( self::getDefaultInfo() ) )->newFile( 'test!' ); $this->assertFalse( $file->getDescriptionText() ); } public function provideLoadFromDBAndCache() { return [ 'legacy' => [ // phpcs:ignore Generic.Files.LineLength '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;}}', [], false, ], 'json' => [ // phpcs:ignore Generic.Files.LineLength '{"data":{"frameCount":0,"loopCount":1,"duration":0,"bitDepth":16,"colorType":"truecolour","metadata":{"DateTime":"2019:07:30 13:52:32","_MW_PNG_VERSION":1}}}', [], false, ], 'json with blobs' => [ // phpcs:ignore Generic.Files.LineLength '{"blobs":{"colorType":"__BLOB0__"},"data":{"frameCount":0,"loopCount":1,"duration":0,"bitDepth":16,"metadata":{"DateTime":"2019:07:30 13:52:32","_MW_PNG_VERSION":1}}}', [ '"truecolour"' ], false, ], 'large (>100KB triggers uncached case)' => [ // phpcs:ignore Generic.Files.LineLength '{"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}}}', [], 102401, ], 'large json blob' => [ // phpcs:ignore Generic.Files.LineLength '{"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}}}', [ '"' . str_repeat( 'x', 102401 ) . '"' ], 102401, ], ]; } /** * Test loadFromDB() and loadFromCache() and helpers * * @dataProvider provideLoadFromDBAndCache * @covers File * @covers LocalFile * @param string $meta * @param array $blobs Metadata blob values * @param int|false $largeItemSize The size of the "large" metadata item, * or false if there will be no such item. */ public function testLoadFromDBAndCache( $meta, $blobs, $largeItemSize ) { $services = MediaWikiServices::getInstance(); $cache = new HashBagOStuff; $this->setService( 'MainWANObjectCache', new WANObjectCache( [ 'cache' => $cache ] ) ); $dbw = wfGetDB( DB_PRIMARY ); $norm = $services->getActorNormalization(); $user = $this->getTestSysop()->getUserIdentity(); $actorId = $norm->acquireActorId( $user, $dbw ); $comment = $services->getCommentStore()->createComment( $dbw, 'comment' ); $title = Title::newFromText( 'File:Random-11m.png' ); if ( $blobs ) { $blobStore = $services->getBlobStore(); foreach ( $blobs as $i => $value ) { $address = $blobStore->storeBlob( $value ); $meta = str_replace( "__BLOB{$i}__", $address, $meta ); } } // The provided metadata strings should all unserialize to this $expectedMetaArray = [ 'frameCount' => 0, 'loopCount' => 1, 'duration' => 0.0, 'bitDepth' => 16, 'colorType' => 'truecolour', 'metadata' => [ 'DateTime' => '2019:07:30 13:52:32', '_MW_PNG_VERSION' => 1, ], ]; if ( $largeItemSize ) { $expectedMetaArray['large'] = str_repeat( 'x', $largeItemSize ); } $expectedProps = [ 'name' => 'Random-11m.png', 'size' => 10816824, 'width' => 1000, 'height' => 1800, 'metadata' => $expectedMetaArray, 'bits' => 16, 'media_type' => 'BITMAP', 'mime' => 'image/png', 'timestamp' => '20201105235242', 'sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru' ]; $dbw->insert( 'image', [ 'img_name' => 'Random-11m.png', 'img_size' => 10816824, 'img_width' => 1000, 'img_height' => 1800, 'img_metadata' => $meta, 'img_bits' => 16, 'img_media_type' => 'BITMAP', 'img_major_mime' => 'image', 'img_minor_mime' => 'png', 'img_description_id' => $comment->id, 'img_actor' => $actorId, 'img_timestamp' => $dbw->timestamp( '20201105235242' ), 'img_sha1' => 'sy02psim0bgdh0jt4vdltuzoh7j80ru', ] ); $repo = $services->getRepoGroup()->getLocalRepo(); $file = $repo->findFile( $title ); $this->assertFileProperties( $expectedProps, $file ); $this->assertSame( 'truecolour', $file->getMetadataItem( 'colorType' ) ); $this->assertSame( [ 'loopCount' => 1, 'bitDepth' => 16 ], $file->getMetadataItems( [ 'loopCount', 'bitDepth', 'nonexistent' ] ) ); $this->assertSame( 'comment', $file->getDescription() ); $this->assertTrue( $user->equals( $file->getUploader() ) ); // Test cache by corrupting DB // Don't wipe img_metadata though since that will be loaded by loadExtraFromDB() $dbw->update( 'image', [ 'img_size' => 0 ], [ 'img_name' => 'Random-11m.png' ], __METHOD__ ); $file = LocalFile::newFromTitle( $title, $repo ); $this->assertFileProperties( $expectedProps, $file ); $this->assertSame( 'truecolour', $file->getMetadataItem( 'colorType' ) ); $this->assertSame( [ 'loopCount' => 1, 'bitDepth' => 16 ], $file->getMetadataItems( [ 'loopCount', 'bitDepth', 'nonexistent' ] ) ); $this->assertSame( 'comment', $file->getDescription() ); $this->assertTrue( $user->equals( $file->getUploader() ) ); // Make sure we were actually hitting the WAN cache $dbw->delete( 'image', [ 'img_name' => 'Random-11m.png' ], __METHOD__ ); $file->invalidateCache(); $file = LocalFile::newFromTitle( $title, $repo ); $this->assertSame( false, $file->exists() ); } private function assertFileProperties( $expectedProps, $file ) { // Compare metadata without ordering if ( isset( $expectedProps['metadata'] ) ) { $this->assertArrayEquals( $expectedProps['metadata'], $file->getMetadataArray() ); } // Filter out unsupported expected properties $expectedProps = array_intersect_key( $expectedProps, array_fill_keys( [ 'name', 'size', 'width', 'height', 'bits', 'media_type', 'mime', 'timestamp', 'sha1' ], true ) ); // Compare the other properties $actualProps = [ 'name' => $file->getName(), 'size' => $file->getSize(), 'width' => $file->getWidth(), 'height' => $file->getHeight(), 'bits' => $file->getBitDepth(), 'media_type' => $file->getMediaType(), 'mime' => $file->getMimeType(), 'timestamp' => $file->getTimestamp(), 'sha1' => $file->getSha1() ]; $actualProps = array_intersect_key( $actualProps, $expectedProps ); $this->assertArrayEquals( $expectedProps, $actualProps, false, true ); } public function provideLegacyMetadataRoundTrip() { return [ [ '0' ], [ '-1' ], [ '' ] ]; } /** * Test the legacy function LocalFile::getMetadata() * @dataProvider provideLegacyMetadataRoundTrip * @covers LocalFile */ public function testLegacyMetadataRoundTrip( $meta ) { $file = new class( $meta ) extends LocalFile { public function __construct( $meta ) { $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo(); parent::__construct( Title::newFromText( 'File:TestLegacyMetadataRoundTrip' ), $repo ); $this->loadMetadataFromString( $meta ); $this->dataLoaded = true; } }; $this->assertSame( $meta, $file->getMetadata() ); } public function provideRecordUpload3() { $files = [ 'test.jpg' => [ 'width' => 20, 'height' => 20, 'bits' => 8, 'metadata' => [ 'ImageDescription' => 'Test file', 'XResolution' => '72/1', 'YResolution' => '72/1', 'ResolutionUnit' => 2, 'YCbCrPositioning' => 1, 'JPEGFileComment' => [ 'Created with GIMP', ], 'MEDIAWIKI_EXIF_VERSION' => 2, ], 'fileExists' => true, 'size' => 437, 'file-mime' => 'image/jpeg', 'major_mime' => 'image', 'minor_mime' => 'jpeg', 'mime' => 'image/jpeg', 'sha1' => '620ezvucfyia1mltnavzpqg9gmai2gf', 'media_type' => 'BITMAP', ], 'large-text.pdf' => [ 'width' => 1275, 'height' => 1650, 'fileExists' => true, 'size' => 10598657, 'file-mime' => 'application/pdf', 'major_mime' => 'application', 'minor_mime' => 'pdf', 'mime' => 'application/pdf', 'sha1' => '1o3l1yqjue2diq07grnnyq9kyapfpor', 'bits' => 0, 'media_type' => 'OFFICE', 'metadata' => [ 'Pages' => '6', 'text' => [ 'Page 1 text .................................', 'Page 2 text .................................', 'Page 3 text .................................', 'Page 4 text .................................', 'Page 5 text .................................', 'Page 6 text .................................', ] ] ], 'no-text.pdf' => [ 'width' => 1275, 'height' => 1650, 'fileExists' => true, 'size' => 10598657, 'file-mime' => 'application/pdf', 'major_mime' => 'application', 'minor_mime' => 'pdf', 'mime' => 'application/pdf', 'sha1' => '1o3l1yqjue2diq07grnnyq9kyapfpor', 'bits' => 0, 'media_type' => 'OFFICE', 'metadata' => [ 'Pages' => '6', ] ] ]; $configurations = [ [], [ 'useJsonMetadata' => true ], [ 'useJsonMetadata' => true, 'useSplitMetadata' => true, 'splitMetadataThreshold' => 50 ] ]; return ArrayUtils::cartesianProduct( $files, $configurations ); } private function getMockPdfHandler() { return new class extends ImageHandler { public function doTransform( $image, $dstPath, $dstUrl, $params, $flags = 0 ) { } public function useSplitMetadata() { return true; } }; } /** * Test recordUpload3() and confirm that file properties are reflected back * after loading the new file from the DB. * * @covers LocalFile * @dataProvider provideRecordUpload3 * @param array $props File properties * @param array $conf LocalRepo configuration overrides */ public function testRecordUpload3( $props, $conf ) { $repo = new LocalRepo( [ 'class' => LocalRepo::class, 'name' => 'test', 'backend' => new FSFileBackend( [ 'name' => 'test-backend', 'wikiId' => WikiMap::getCurrentWikiId(), 'basePath' => '/nonexistent' ] ) ] + $conf ); $title = Title::newFromText( 'File:Test.jpg' ); $file = new LocalFile( $title, $repo ); if ( $props['mime'] === 'application/pdf' ) { TestingAccessWrapper::newFromObject( $file )->handler = $this->getMockPdfHandler(); } $status = $file->recordUpload3( 'oldver', 'comment', 'page text', $this->getTestSysop()->getUser(), $props ); $this->assertSame( [], $status->getErrors() ); // Check properties of the same object immediately after upload $this->assertFileProperties( $props, $file ); // Check round-trip through the DB $file = new LocalFile( $title, $repo ); $this->assertFileProperties( $props, $file ); } /** * @covers LocalFile */ public function testUpload() { $repo = new LocalRepo( [ 'class' => LocalRepo::class, 'name' => 'test', 'backend' => new FSFileBackend( [ 'name' => 'test-backend', 'wikiId' => WikiMap::getCurrentWikiId(), 'basePath' => $this->getNewTempDirectory() ] ) ] ); $title = Title::newFromText( 'File:Test.jpg' ); $file = new LocalFile( $title, $repo ); $path = __DIR__ . '/../../../data/media/test.jpg'; $status = $file->upload( $path, 'comment', 'page text', 0 ); $this->assertSame( [], $status->getErrors() ); // Test reupload $file = new LocalFile( $title, $repo ); $path = __DIR__ . '/../../../data/media/jpeg-xmp-nullchar.jpg'; $status = $file->upload( $path, 'comment', 'page text', 0 ); $this->assertSame( [], $status->getErrors() ); } public function provideReserializeMetadata() { return [ [ '', '' ], [ 'a:1:{s:4:"test";i:1;}', '{"data":{"test":1}}' ], [ serialize( [ 'test' => str_repeat( 'x', 100 ) ] ), '{"data":[],"blobs":{"test":"tt:%d"}}' ] ]; } /** * Test reserializeMetadata() via maybeUpgradeRow() * * @covers LocalFile::maybeUpgradeRow * @covers LocalFile::reserializeMetadata * @dataProvider provideReserializeMetadata */ public function testReserializeMetadata( $input, $expected ) { $dbw = wfGetDB( DB_PRIMARY ); $services = MediaWikiServices::getInstance(); $norm = $services->getActorNormalization(); $user = $this->getTestSysop()->getUserIdentity(); $actorId = $norm->acquireActorId( $user, $dbw ); $comment = $services->getCommentStore()->createComment( $dbw, 'comment' ); $dbw->insert( 'image', [ 'img_name' => 'Test.pdf', 'img_size' => 1, 'img_width' => 1, 'img_height' => 1, 'img_metadata' => $input, 'img_bits' => 0, 'img_media_type' => 'OFFICE', 'img_major_mime' => 'application', 'img_minor_mime' => 'pdf', 'img_description_id' => $comment->id, 'img_actor' => $actorId, 'img_timestamp' => $dbw->timestamp( '20201105235242' ), 'img_sha1' => 'hhhh', ] ); $repo = new LocalRepo( [ 'class' => LocalRepo::class, 'name' => 'test', 'useJsonMetadata' => true, 'useSplitMetadata' => true, 'splitMetadataThreshold' => 50, 'updateCompatibleMetadata' => true, 'reserializeMetadata' => true, 'backend' => new FSFileBackend( [ 'name' => 'test-backend', 'wikiId' => WikiMap::getCurrentWikiId(), 'basePath' => '/nonexistent' ] ) ] ); $title = Title::newFromText( 'File:Test.pdf' ); $file = new LocalFile( $title, $repo ); TestingAccessWrapper::newFromObject( $file )->handler = $this->getMockPdfHandler(); $file->load(); $file->maybeUpgradeRow(); $metadata = $dbw->selectField( 'image', 'img_metadata', [ 'img_name' => 'Test.pdf' ], __METHOD__ ); $this->assertStringMatchesFormat( $expected, $metadata ); } /** * Test upgradeRow() via maybeUpgradeRow() * * @covers LocalFile::maybeUpgradeRow * @covers LocalFile::upgradeRow */ public function testUpgradeRow() { $repo = new LocalRepo( [ 'class' => LocalRepo::class, 'name' => 'test', 'updateCompatibleMetadata' => true, 'useJsonMetadata' => true, 'hashLevels' => 0, 'backend' => new FSFileBackend( [ 'name' => 'test-backend', 'wikiId' => WikiMap::getCurrentWikiId(), 'containerPaths' => [ 'test-public' => __DIR__ . '/../../../data/media' ] ] ) ] ); $dbw = wfGetDB( DB_PRIMARY ); $services = MediaWikiServices::getInstance(); $norm = $services->getActorNormalization(); $user = $this->getTestSysop()->getUserIdentity(); $actorId = $norm->acquireActorId( $user, $dbw ); $comment = $services->getCommentStore()->createComment( $dbw, 'comment' ); $dbw->insert( 'image', [ 'img_name' => 'Png-native-test.png', 'img_size' => 1, 'img_width' => 1, 'img_height' => 1, 'img_metadata' => 'a:1:{s:8:"metadata";a:1:{s:15:"_MW_PNG_VERSION";i:0;}}', 'img_bits' => 0, 'img_media_type' => 'OFFICE', 'img_major_mime' => 'image', 'img_minor_mime' => 'png', 'img_description_id' => $comment->id, 'img_actor' => $actorId, 'img_timestamp' => $dbw->timestamp( '20201105235242' ), 'img_sha1' => 'hhhh', ] ); $title = Title::newFromText( 'File:Png-native-test.png' ); $file = new LocalFile( $title, $repo ); $file->load(); $file->maybeUpgradeRow(); $metadata = $dbw->selectField( 'image', 'img_metadata', [ 'img_name' => 'Png-native-test.png' ] ); // Just confirm that it looks like JSON with real metadata $this->assertStringStartsWith( '{"data":{"frameCount":0,', $metadata ); $file = new LocalFile( $title, $repo ); $this->assertFileProperties( [ 'size' => 4665, 'width' => 420, 'height' => 300, 'sha1' => '3n69qtiaif1swp3kyfueqjtmw2u4c2b', 'bits' => 8, 'media_type' => 'BITMAP', ], $file ); } }