* Add automatic splitting of large metadata on upload or refresh. If
the reserializeMetadata option is enabled, metadata stored with PHP
serialization will be automatically reserialized to JSON.
* Inject configuration variable $wgUpdateCompatibleMetadata via
LocalRepo instead of accessing it directly.
* In refreshImageMetadata.php and rebuildImages.php, construct a new
LocalRepo with config overrides, instead of overwriting config
globals. Add a helper to RepoGroup to help with this.
* In refreshImageMetadata.php, add new options --convert-to-json and
--split which reserialize metadata and optionally split out large
items to blob storage.
Also, refreshImageMetadata.php was totally broken in the non-force mode
since metadata refresh on page view was disabled in b814245d9f. The
maintenance script was relying on newFileFromRow() magically upgrading
the row, which doesn't happen anymore. So, call maybeUpgradeRow()
directly.
Bug: T275268
Change-Id: I7bf7d9cef71641e287ca4346b568b381f4ada50e
1040 lines
30 KiB
PHP
1040 lines
30 KiB
PHP
<?php
|
|
|
|
/**
|
|
* These tests should work regardless of $wgCapitalLinks
|
|
* @todo Split tests into providers and test methods
|
|
*/
|
|
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Permissions\Authority;
|
|
use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait;
|
|
use MediaWiki\User\UserIdentity;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
/**
|
|
* @group Database
|
|
*/
|
|
class LocalFileTest extends MediaWikiIntegrationTestCase {
|
|
use MockAuthorityTrait;
|
|
|
|
protected function setUp(): void {
|
|
parent::setUp();
|
|
$this->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 File::getUser
|
|
*/
|
|
public function testGetUserForNonExistingFile() {
|
|
$this->hideDeprecated( 'File::getUser' );
|
|
$file = ( new LocalRepo( self::getDefaultInfo() ) )->newFile( 'test!' );
|
|
$this->assertSame( 'Unknown user', $file->getUser() );
|
|
}
|
|
|
|
/**
|
|
* @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 );
|
|
}
|
|
}
|