This makes sure that length and hash are calculated if not known. This patch also adds missing unit tests for RevisionArchiveRecord, and consolidates unit tests for the different RevisionRecord subclasses using a trait. Bug: T192189 Change-Id: I5e1d17ba96e61e068b6aa5ac9c45ac0f657905a6
512 lines
12 KiB
PHP
512 lines
12 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\Storage;
|
|
|
|
use CommentStoreComment;
|
|
use LogicException;
|
|
use MediaWiki\Storage\RevisionRecord;
|
|
use MediaWiki\Storage\RevisionSlots;
|
|
use MediaWiki\Storage\RevisionStoreRecord;
|
|
use MediaWiki\Storage\SlotRecord;
|
|
use MediaWiki\Storage\SuppressedDataException;
|
|
use MediaWiki\User\UserIdentityValue;
|
|
use TextContent;
|
|
use Title;
|
|
|
|
// PHPCS should not complain about @covers and @dataProvider being used in traits, see T192384
|
|
// phpcs:disable MediaWiki.Commenting.PhpunitAnnotations.NotTestClass
|
|
|
|
/**
|
|
* @covers \MediaWiki\Storage\RevisionRecord
|
|
*
|
|
* @note Expects to be used in classes that extend MediaWikiTestCase.
|
|
*/
|
|
trait RevisionRecordTests {
|
|
|
|
/**
|
|
* @param array $rowOverrides
|
|
*
|
|
* @return RevisionRecord
|
|
*/
|
|
protected abstract function newRevision( array $rowOverrides = [] );
|
|
|
|
private function provideAudienceCheckData( $field ) {
|
|
yield 'field accessible for oversighter (ALL)' => [
|
|
RevisionRecord::SUPPRESSED_ALL,
|
|
[ 'oversight' ],
|
|
true,
|
|
false
|
|
];
|
|
|
|
yield 'field accessible for oversighter' => [
|
|
RevisionRecord::DELETED_RESTRICTED | $field,
|
|
[ 'oversight' ],
|
|
true,
|
|
false
|
|
];
|
|
|
|
yield 'field not accessible for sysops (ALL)' => [
|
|
RevisionRecord::SUPPRESSED_ALL,
|
|
[ 'sysop' ],
|
|
false,
|
|
false
|
|
];
|
|
|
|
yield 'field not accessible for sysops' => [
|
|
RevisionRecord::DELETED_RESTRICTED | $field,
|
|
[ 'sysop' ],
|
|
false,
|
|
false
|
|
];
|
|
|
|
yield 'field accessible for sysops' => [
|
|
$field,
|
|
[ 'sysop' ],
|
|
true,
|
|
false
|
|
];
|
|
|
|
yield 'field suppressed for logged in users' => [
|
|
$field,
|
|
[ 'user' ],
|
|
false,
|
|
false
|
|
];
|
|
|
|
yield 'unrelated field suppressed' => [
|
|
$field === RevisionRecord::DELETED_COMMENT
|
|
? RevisionRecord::DELETED_USER
|
|
: RevisionRecord::DELETED_COMMENT,
|
|
[ 'user' ],
|
|
true,
|
|
true
|
|
];
|
|
|
|
yield 'nothing suppressed' => [
|
|
0,
|
|
[ 'user' ],
|
|
true,
|
|
true
|
|
];
|
|
}
|
|
|
|
public function testSerialization_fails() {
|
|
$this->setExpectedException( LogicException::class );
|
|
$rev = $this->newRevision();
|
|
serialize( $rev );
|
|
}
|
|
|
|
public function provideGetComment_audience() {
|
|
return $this->provideAudienceCheckData( RevisionRecord::DELETED_COMMENT );
|
|
}
|
|
|
|
private function forceStandardPermissions() {
|
|
$this->setMwGlobals(
|
|
'wgGroupPermissions',
|
|
[
|
|
'user' => [
|
|
'viewsuppressed' => false,
|
|
'suppressrevision' => false,
|
|
'deletedtext' => false,
|
|
'deletedhistory' => false,
|
|
],
|
|
'sysop' => [
|
|
'viewsuppressed' => false,
|
|
'suppressrevision' => false,
|
|
'deletedtext' => true,
|
|
'deletedhistory' => true,
|
|
],
|
|
'oversight' => [
|
|
'deletedtext' => true,
|
|
'deletedhistory' => true,
|
|
'viewsuppressed' => true,
|
|
'suppressrevision' => true,
|
|
],
|
|
]
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideGetComment_audience
|
|
*/
|
|
public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
|
|
$this->forceStandardPermissions();
|
|
|
|
$user = $this->getTestUser( $groups )->getUser();
|
|
$rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
|
|
|
|
$this->assertNotNull( $rev->getComment( RevisionRecord::RAW ), 'raw can' );
|
|
|
|
$this->assertSame(
|
|
$publicCan,
|
|
$rev->getComment( RevisionRecord::FOR_PUBLIC ) !== null,
|
|
'public can'
|
|
);
|
|
$this->assertSame(
|
|
$userCan,
|
|
$rev->getComment( RevisionRecord::FOR_THIS_USER, $user ) !== null,
|
|
'user can'
|
|
);
|
|
}
|
|
|
|
public function provideGetUser_audience() {
|
|
return $this->provideAudienceCheckData( RevisionRecord::DELETED_USER );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideGetUser_audience
|
|
*/
|
|
public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
|
|
$this->forceStandardPermissions();
|
|
|
|
$user = $this->getTestUser( $groups )->getUser();
|
|
$rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
|
|
|
|
$this->assertNotNull( $rev->getUser( RevisionRecord::RAW ), 'raw can' );
|
|
|
|
$this->assertSame(
|
|
$publicCan,
|
|
$rev->getUser( RevisionRecord::FOR_PUBLIC ) !== null,
|
|
'public can'
|
|
);
|
|
$this->assertSame(
|
|
$userCan,
|
|
$rev->getUser( RevisionRecord::FOR_THIS_USER, $user ) !== null,
|
|
'user can'
|
|
);
|
|
}
|
|
|
|
public function provideGetSlot_audience() {
|
|
return $this->provideAudienceCheckData( RevisionRecord::DELETED_TEXT );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideGetSlot_audience
|
|
*/
|
|
public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
|
|
$this->forceStandardPermissions();
|
|
|
|
$user = $this->getTestUser( $groups )->getUser();
|
|
$rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
|
|
|
|
// NOTE: slot meta-data is never suppressed, just the content is!
|
|
$this->assertTrue( $rev->hasSlot( 'main' ), 'hasSlot is never suppressed' );
|
|
$this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::RAW ), 'raw meta' );
|
|
$this->assertNotNull( $rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC ), 'public meta' );
|
|
|
|
$this->assertNotNull(
|
|
$rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user ),
|
|
'user can'
|
|
);
|
|
|
|
try {
|
|
$rev->getSlot( 'main', RevisionRecord::FOR_PUBLIC )->getContent();
|
|
$exception = null;
|
|
} catch ( SuppressedDataException $ex ) {
|
|
$exception = $ex;
|
|
}
|
|
|
|
$this->assertSame(
|
|
$publicCan,
|
|
$exception === null,
|
|
'public can'
|
|
);
|
|
|
|
try {
|
|
$rev->getSlot( 'main', RevisionRecord::FOR_THIS_USER, $user )->getContent();
|
|
$exception = null;
|
|
} catch ( SuppressedDataException $ex ) {
|
|
$exception = $ex;
|
|
}
|
|
|
|
$this->assertSame(
|
|
$userCan,
|
|
$exception === null,
|
|
'user can'
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideGetSlot_audience
|
|
*/
|
|
public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
|
|
$this->forceStandardPermissions();
|
|
|
|
$user = $this->getTestUser( $groups )->getUser();
|
|
$rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
|
|
|
|
$this->assertNotNull( $rev->getContent( 'main', RevisionRecord::RAW ), 'raw can' );
|
|
|
|
$this->assertSame(
|
|
$publicCan,
|
|
$rev->getContent( 'main', RevisionRecord::FOR_PUBLIC ) !== null,
|
|
'public can'
|
|
);
|
|
$this->assertSame(
|
|
$userCan,
|
|
$rev->getContent( 'main', RevisionRecord::FOR_THIS_USER, $user ) !== null,
|
|
'user can'
|
|
);
|
|
}
|
|
|
|
public function testGetSlot() {
|
|
$rev = $this->newRevision();
|
|
|
|
$slot = $rev->getSlot( 'main' );
|
|
$this->assertNotNull( $slot, 'getSlot()' );
|
|
$this->assertSame( 'main', $slot->getRole(), 'getRole()' );
|
|
}
|
|
|
|
public function testHasSlot() {
|
|
$rev = $this->newRevision();
|
|
|
|
$this->assertTrue( $rev->hasSlot( 'main' ) );
|
|
$this->assertFalse( $rev->hasSlot( 'xyz' ) );
|
|
}
|
|
|
|
public function testGetContent() {
|
|
$rev = $this->newRevision();
|
|
|
|
$content = $rev->getSlot( 'main' );
|
|
$this->assertNotNull( $content, 'getContent()' );
|
|
$this->assertSame( CONTENT_MODEL_TEXT, $content->getModel(), 'getModel()' );
|
|
}
|
|
|
|
public function provideUserCanBitfield() {
|
|
yield [ 0, 0, [], null, true ];
|
|
// Bitfields match, user has no permissions
|
|
yield [
|
|
RevisionRecord::DELETED_TEXT,
|
|
RevisionRecord::DELETED_TEXT,
|
|
[],
|
|
null,
|
|
false
|
|
];
|
|
yield [
|
|
RevisionRecord::DELETED_COMMENT,
|
|
RevisionRecord::DELETED_COMMENT,
|
|
[],
|
|
null,
|
|
false,
|
|
];
|
|
yield [
|
|
RevisionRecord::DELETED_USER,
|
|
RevisionRecord::DELETED_USER,
|
|
[],
|
|
null,
|
|
false
|
|
];
|
|
yield [
|
|
RevisionRecord::DELETED_RESTRICTED,
|
|
RevisionRecord::DELETED_RESTRICTED,
|
|
[],
|
|
null,
|
|
false,
|
|
];
|
|
// Bitfields match, user (admin) does have permissions
|
|
yield [
|
|
RevisionRecord::DELETED_TEXT,
|
|
RevisionRecord::DELETED_TEXT,
|
|
[ 'sysop' ],
|
|
null,
|
|
true,
|
|
];
|
|
yield [
|
|
RevisionRecord::DELETED_COMMENT,
|
|
RevisionRecord::DELETED_COMMENT,
|
|
[ 'sysop' ],
|
|
null,
|
|
true,
|
|
];
|
|
yield [
|
|
RevisionRecord::DELETED_USER,
|
|
RevisionRecord::DELETED_USER,
|
|
[ 'sysop' ],
|
|
null,
|
|
true,
|
|
];
|
|
// Bitfields match, user (admin) does not have permissions
|
|
yield [
|
|
RevisionRecord::DELETED_RESTRICTED,
|
|
RevisionRecord::DELETED_RESTRICTED,
|
|
[ 'sysop' ],
|
|
null,
|
|
false,
|
|
];
|
|
// Bitfields match, user (oversight) does have permissions
|
|
yield [
|
|
RevisionRecord::DELETED_RESTRICTED,
|
|
RevisionRecord::DELETED_RESTRICTED,
|
|
[ 'oversight' ],
|
|
null,
|
|
true,
|
|
];
|
|
// Check permissions using the title
|
|
yield [
|
|
RevisionRecord::DELETED_TEXT,
|
|
RevisionRecord::DELETED_TEXT,
|
|
[ 'sysop' ],
|
|
Title::newFromText( __METHOD__ ),
|
|
true,
|
|
];
|
|
yield [
|
|
RevisionRecord::DELETED_TEXT,
|
|
RevisionRecord::DELETED_TEXT,
|
|
[],
|
|
Title::newFromText( __METHOD__ ),
|
|
false,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideUserCanBitfield
|
|
* @covers \MediaWiki\Storage\RevisionRecord::userCanBitfield
|
|
*/
|
|
public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
|
|
$this->forceStandardPermissions();
|
|
|
|
$user = $this->getTestUser( $userGroups )->getUser();
|
|
|
|
$this->assertSame(
|
|
$expected,
|
|
RevisionRecord::userCanBitfield( $bitField, $field, $user, $title )
|
|
);
|
|
}
|
|
|
|
public function provideHasSameContent() {
|
|
/**
|
|
* @param SlotRecord[] $slots
|
|
* @param int $revId
|
|
* @return RevisionStoreRecord
|
|
*/
|
|
$recordCreator = function ( array $slots, $revId ) {
|
|
$title = Title::newFromText( 'provideHasSameContent' );
|
|
$title->resetArticleID( 19 );
|
|
$slots = new RevisionSlots( $slots );
|
|
|
|
return new RevisionStoreRecord(
|
|
$title,
|
|
new UserIdentityValue( 11, __METHOD__, 0 ),
|
|
CommentStoreComment::newUnsavedComment( __METHOD__ ),
|
|
(object)[
|
|
'rev_id' => strval( $revId ),
|
|
'rev_page' => strval( $title->getArticleID() ),
|
|
'rev_timestamp' => '20200101000000',
|
|
'rev_deleted' => 0,
|
|
'rev_minor_edit' => 0,
|
|
'rev_parent_id' => '5',
|
|
'rev_len' => $slots->computeSize(),
|
|
'rev_sha1' => $slots->computeSha1(),
|
|
'page_latest' => '18',
|
|
],
|
|
$slots
|
|
);
|
|
};
|
|
|
|
// Create some slots with content
|
|
$mainA = SlotRecord::newUnsaved( 'main', new TextContent( 'A' ) );
|
|
$mainB = SlotRecord::newUnsaved( 'main', new TextContent( 'B' ) );
|
|
$auxA = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
|
|
$auxB = SlotRecord::newUnsaved( 'aux', new TextContent( 'A' ) );
|
|
|
|
$initialRecord = $recordCreator( [ $mainA ], 12 );
|
|
|
|
return [
|
|
'same record object' => [
|
|
true,
|
|
$initialRecord,
|
|
$initialRecord,
|
|
],
|
|
'same record content, different object' => [
|
|
true,
|
|
$recordCreator( [ $mainA ], 12 ),
|
|
$recordCreator( [ $mainA ], 13 ),
|
|
],
|
|
'same record content, aux slot, different object' => [
|
|
true,
|
|
$recordCreator( [ $auxA ], 12 ),
|
|
$recordCreator( [ $auxB ], 13 ),
|
|
],
|
|
'different content' => [
|
|
false,
|
|
$recordCreator( [ $mainA ], 12 ),
|
|
$recordCreator( [ $mainB ], 13 ),
|
|
],
|
|
'different content and number of slots' => [
|
|
false,
|
|
$recordCreator( [ $mainA ], 12 ),
|
|
$recordCreator( [ $mainA, $mainB ], 13 ),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideHasSameContent
|
|
* @covers \MediaWiki\Storage\RevisionRecord::hasSameContent
|
|
* @group Database
|
|
*/
|
|
public function testHasSameContent(
|
|
$expected,
|
|
RevisionRecord $record1,
|
|
RevisionRecord $record2
|
|
) {
|
|
$this->assertSame(
|
|
$expected,
|
|
$record1->hasSameContent( $record2 )
|
|
);
|
|
}
|
|
|
|
public function provideIsDeleted() {
|
|
yield 'no deletion' => [
|
|
0,
|
|
[
|
|
RevisionRecord::DELETED_TEXT => false,
|
|
RevisionRecord::DELETED_COMMENT => false,
|
|
RevisionRecord::DELETED_USER => false,
|
|
RevisionRecord::DELETED_RESTRICTED => false,
|
|
]
|
|
];
|
|
yield 'text deleted' => [
|
|
RevisionRecord::DELETED_TEXT,
|
|
[
|
|
RevisionRecord::DELETED_TEXT => true,
|
|
RevisionRecord::DELETED_COMMENT => false,
|
|
RevisionRecord::DELETED_USER => false,
|
|
RevisionRecord::DELETED_RESTRICTED => false,
|
|
]
|
|
];
|
|
yield 'text and comment deleted' => [
|
|
RevisionRecord::DELETED_TEXT + RevisionRecord::DELETED_COMMENT,
|
|
[
|
|
RevisionRecord::DELETED_TEXT => true,
|
|
RevisionRecord::DELETED_COMMENT => true,
|
|
RevisionRecord::DELETED_USER => false,
|
|
RevisionRecord::DELETED_RESTRICTED => false,
|
|
]
|
|
];
|
|
yield 'all 4 deleted' => [
|
|
RevisionRecord::DELETED_TEXT +
|
|
RevisionRecord::DELETED_COMMENT +
|
|
RevisionRecord::DELETED_RESTRICTED +
|
|
RevisionRecord::DELETED_USER,
|
|
[
|
|
RevisionRecord::DELETED_TEXT => true,
|
|
RevisionRecord::DELETED_COMMENT => true,
|
|
RevisionRecord::DELETED_USER => true,
|
|
RevisionRecord::DELETED_RESTRICTED => true,
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideIsDeleted
|
|
* @covers \MediaWiki\Storage\RevisionRecord::isDeleted
|
|
*/
|
|
public function testIsDeleted( $revDeleted, $assertionMap ) {
|
|
$rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
|
|
foreach ( $assertionMap as $deletionLevel => $expected ) {
|
|
$this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );
|
|
}
|
|
}
|
|
|
|
}
|