3 namespace MediaWiki\Tests\Storage
;
5 use CommentStoreComment
;
6 use InvalidArgumentException
;
8 use MediaWiki\Storage\RevisionRecord
;
9 use MediaWiki\Storage\RevisionSlots
;
10 use MediaWiki\Storage\RevisionStoreRecord
;
11 use MediaWiki\Storage\SlotRecord
;
12 use MediaWiki\Storage\SuppressedDataException
;
13 use MediaWiki\User\UserIdentity
;
14 use MediaWiki\User\UserIdentityValue
;
15 use MediaWikiTestCase
;
20 * @covers MediaWiki\Storage\RevisionStoreRecord
22 class RevisionStoreRecordTest
extends MediaWikiTestCase
{
25 * @param array $overrides
26 * @return RevisionStoreRecord
28 public function newRevision( array $overrides = [] ) {
29 $title = Title
::newFromText( 'Dummy' );
30 $title->resetArticleID( 17 );
32 $user = new UserIdentityValue( 11, 'Tester' );
33 $comment = CommentStoreComment
::newUnsavedComment( 'Hello World' );
35 $main = SlotRecord
::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
36 $aux = SlotRecord
::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
37 $slots = new RevisionSlots( [ $main, $aux ] );
41 'rev_page' => strval( $title->getArticleID() ),
42 'rev_timestamp' => '20200101000000',
44 'rev_minor_edit' => 0,
45 'rev_parent_id' => '5',
46 'rev_len' => $slots->computeSize(),
47 'rev_sha1' => $slots->computeSha1(),
48 'page_latest' => '18',
51 $row = array_merge( $row, $overrides );
53 return new RevisionStoreRecord( $title, $user, $comment, (object)$row, $slots );
56 public function provideConstructor() {
57 $title = Title
::newFromText( 'Dummy' );
58 $title->resetArticleID( 17 );
60 $user = new UserIdentityValue( 11, 'Tester' );
61 $comment = CommentStoreComment
::newUnsavedComment( 'Hello World' );
63 $main = SlotRecord
::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
64 $aux = SlotRecord
::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
65 $slots = new RevisionSlots( [ $main, $aux ] );
69 'rev_page' => strval( $title->getArticleID() ),
70 'rev_timestamp' => '20200101000000',
72 'rev_minor_edit' => 0,
73 'rev_parent_id' => '5',
74 'rev_len' => $slots->computeSize(),
75 'rev_sha1' => $slots->computeSha1(),
76 'page_latest' => '18',
90 $row['rev_minor_edit'] = '1';
91 $row['rev_deleted'] = strval( RevisionRecord
::DELETED_USER
);
93 yield
'minor deleted' => [
102 $row['page_latest'] = $row['rev_id'];
113 unset( $row['rev_parent'] );
115 yield
'no parent' => [
124 unset( $row['rev_len'] );
125 unset( $row['rev_sha1'] );
127 yield
'no length, no hash' => [
136 yield
'no length, no hash' => [
137 Title
::newFromText( 'DummyDoesNotExist' ),
146 * @dataProvider provideConstructor
148 * @param Title $title
149 * @param UserIdentity $user
150 * @param CommentStoreComment $comment
152 * @param RevisionSlots $slots
153 * @param bool $wikiId
155 public function testConstructorAndGetters(
158 CommentStoreComment
$comment,
160 RevisionSlots
$slots,
163 $rec = new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
165 $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
166 $this->assertSame( $user, $rec->getUser( RevisionRecord
::RAW
), 'getUser' );
167 $this->assertSame( $comment, $rec->getComment(), 'getComment' );
169 $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
170 $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
172 $this->assertSame( (int)$row->rev_id
, $rec->getId(), 'getId' );
173 $this->assertSame( (int)$row->rev_page
, $rec->getPageId(), 'getId' );
174 $this->assertSame( $row->rev_timestamp
, $rec->getTimestamp(), 'getTimestamp' );
175 $this->assertSame( (int)$row->rev_deleted
, $rec->getVisibility(), 'getVisibility' );
176 $this->assertSame( (bool)$row->rev_minor_edit
, $rec->isMinor(), 'getIsMinor' );
178 if ( isset( $row->rev_parent_id
) ) {
179 $this->assertSame( (int)$row->rev_parent_id
, $rec->getParentId(), 'getParentId' );
181 $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
184 if ( isset( $row->rev_len
) ) {
185 $this->assertSame( (int)$row->rev_len
, $rec->getSize(), 'getSize' );
187 $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
190 if ( isset( $row->rev_sha1
) ) {
191 $this->assertSame( $row->rev_sha1
, $rec->getSha1(), 'getSha1' );
193 $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
196 if ( isset( $row->page_latest
) ) {
198 (int)$row->rev_id
=== (int)$row->page_latest
,
211 public function provideConstructorFailure() {
212 $title = Title
::newFromText( 'Dummy' );
213 $title->resetArticleID( 17 );
215 $user = new UserIdentityValue( 11, 'Tester' );
217 $comment = CommentStoreComment
::newUnsavedComment( 'Hello World' );
219 $main = SlotRecord
::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
220 $aux = SlotRecord
::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
221 $slots = new RevisionSlots( [ $main, $aux ] );
225 'rev_page' => strval( $title->getArticleID() ),
226 'rev_timestamp' => '20200101000000',
228 'rev_minor_edit' => 0,
229 'rev_parent_id' => '5',
230 'rev_len' => $slots->computeSize(),
231 'rev_sha1' => $slots->computeSha1(),
232 'page_latest' => '18',
235 yield
'not a row' => [
245 $row['rev_timestamp'] = 'kittens';
247 yield
'bad timestamp' => [
256 $row['rev_page'] = 99;
258 yield
'page ID mismatch' => [
268 yield
'bad wiki' => [
279 * @dataProvider provideConstructorFailure
281 * @param Title $title
282 * @param UserIdentity $user
283 * @param CommentStoreComment $comment
285 * @param RevisionSlots $slots
286 * @param bool $wikiId
288 public function testConstructorFailure(
291 CommentStoreComment
$comment,
293 RevisionSlots
$slots,
296 $this->setExpectedException( InvalidArgumentException
::class );
297 new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
300 private function provideAudienceCheckData( $field ) {
301 yield
'field accessible for oversighter (ALL)' => [
302 Revisionrecord
::SUPPRESSED_ALL
,
308 yield
'field accessible for oversighter' => [
309 Revisionrecord
::DELETED_RESTRICTED |
$field,
315 yield
'field not accessible for sysops (ALL)' => [
316 Revisionrecord
::SUPPRESSED_ALL
,
322 yield
'field not accessible for sysops' => [
323 Revisionrecord
::DELETED_RESTRICTED |
$field,
329 yield
'field accessible for sysops' => [
336 yield
'field suppressed for logged in users' => [
343 yield
'unrelated field suppressed' => [
344 $field === Revisionrecord
::DELETED_COMMENT
345 ? Revisionrecord
::DELETED_USER
346 : Revisionrecord
::DELETED_COMMENT
,
352 yield
'nothing suppressed' => [
360 public function testSerialization_fails() {
361 $this->setExpectedException( LogicException
::class );
362 $rev = $this->newRevision();
366 public function provideGetComment_audience() {
367 return $this->provideAudienceCheckData( RevisionRecord
::DELETED_COMMENT
);
370 private function forceStandardPermissions() {
372 'wgGroupPermissions',
375 'viewsuppressed' => false,
376 'suppressrevision' => false,
377 'deletedtext' => false,
378 'deletedhistory' => false,
381 'viewsuppressed' => false,
382 'suppressrevision' => false,
383 'deletedtext' => true,
384 'deletedhistory' => true,
387 'deletedtext' => true,
388 'deletedhistory' => true,
389 'viewsuppressed' => true,
390 'suppressrevision' => true,
397 * @dataProvider provideGetComment_audience
399 public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
400 $this->forceStandardPermissions();
402 $user = $this->getTestUser( $groups )->getUser();
403 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
405 $this->assertNotNull( $rev->getComment( RevisionRecord
::RAW
), 'raw can' );
409 $rev->getComment( RevisionRecord
::FOR_PUBLIC
) !== null,
414 $rev->getComment( RevisionRecord
::FOR_THIS_USER
, $user ) !== null,
419 public function provideGetUser_audience() {
420 return $this->provideAudienceCheckData( RevisionRecord
::DELETED_USER
);
424 * @dataProvider provideGetUser_audience
426 public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
427 $this->forceStandardPermissions();
429 $user = $this->getTestUser( $groups )->getUser();
430 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
432 $this->assertNotNull( $rev->getUser( RevisionRecord
::RAW
), 'raw can' );
436 $rev->getUser( RevisionRecord
::FOR_PUBLIC
) !== null,
441 $rev->getUser( RevisionRecord
::FOR_THIS_USER
, $user ) !== null,
446 public function provideGetSlot_audience() {
447 return $this->provideAudienceCheckData( RevisionRecord
::DELETED_TEXT
);
451 * @dataProvider provideGetSlot_audience
453 public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
454 $this->forceStandardPermissions();
456 $user = $this->getTestUser( $groups )->getUser();
457 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
459 // NOTE: slot meta-data is never suppressed, just the content is!
460 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord
::RAW
), 'raw can' );
461 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord
::FOR_PUBLIC
), 'public can' );
463 $this->assertNotNull(
464 $rev->getSlot( 'main', RevisionRecord
::FOR_THIS_USER
, $user ),
469 $rev->getSlot( 'main', RevisionRecord
::FOR_PUBLIC
)->getContent();
471 } catch ( SuppressedDataException
$ex ) {
482 $rev->getSlot( 'main', RevisionRecord
::FOR_THIS_USER
, $user )->getContent();
484 } catch ( SuppressedDataException
$ex ) {
495 public function provideGetSlot_audience_latest() {
496 return $this->provideAudienceCheckData( RevisionRecord
::DELETED_TEXT
);
500 * @dataProvider provideGetSlot_audience_latest
502 public function testGetSlot_audience_latest( $visibility, $groups, $userCan, $publicCan ) {
503 $this->forceStandardPermissions();
505 $user = $this->getTestUser( $groups )->getUser();
506 $rev = $this->newRevision(
508 'rev_deleted' => $visibility,
510 'page_latest' => 11, // revision is current
515 $this->assertTrue( $rev->isCurrent(), 'isCurrent()' );
517 // NOTE: slot meta-data is never suppressed, just the content is!
518 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord
::RAW
), 'raw can' );
519 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord
::FOR_PUBLIC
), 'public can' );
521 $this->assertNotNull(
522 $rev->getSlot( 'main', RevisionRecord
::FOR_THIS_USER
, $user ),
526 // NOTE: the content of the current revision is never suppressed!
527 // Check that getContent() doesn't throw SuppressedDataException
528 $rev->getSlot( 'main', RevisionRecord
::RAW
)->getContent();
529 $rev->getSlot( 'main', RevisionRecord
::FOR_PUBLIC
)->getContent();
530 $rev->getSlot( 'main', RevisionRecord
::FOR_THIS_USER
, $user )->getContent();
534 * @dataProvider provideGetSlot_audience
536 public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
537 $this->forceStandardPermissions();
539 $user = $this->getTestUser( $groups )->getUser();
540 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
542 $this->assertNotNull( $rev->getContent( 'main', RevisionRecord
::RAW
), 'raw can' );
546 $rev->getContent( 'main', RevisionRecord
::FOR_PUBLIC
) !== null,
551 $rev->getContent( 'main', RevisionRecord
::FOR_THIS_USER
, $user ) !== null,
556 public function testGetSlot() {
557 $rev = $this->newRevision();
559 $slot = $rev->getSlot( 'main' );
560 $this->assertNotNull( $slot, 'getSlot()' );
561 $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
564 public function testGetContent() {
565 $rev = $this->newRevision();
567 $content = $rev->getSlot( 'main' );
568 $this->assertNotNull( $content, 'getContent()' );
569 $this->assertSame( CONTENT_MODEL_TEXT
, $content->getModel(), 'getModel()' );
572 public function provideUserCanBitfield() {
573 yield
[ 0, 0, [], null, true ];
574 // Bitfields match, user has no permissions
576 RevisionRecord
::DELETED_TEXT
,
577 RevisionRecord
::DELETED_TEXT
,
583 RevisionRecord
::DELETED_COMMENT
,
584 RevisionRecord
::DELETED_COMMENT
,
590 RevisionRecord
::DELETED_USER
,
591 RevisionRecord
::DELETED_USER
,
597 RevisionRecord
::DELETED_RESTRICTED
,
598 RevisionRecord
::DELETED_RESTRICTED
,
603 // Bitfields match, user (admin) does have permissions
605 RevisionRecord
::DELETED_TEXT
,
606 RevisionRecord
::DELETED_TEXT
,
612 RevisionRecord
::DELETED_COMMENT
,
613 RevisionRecord
::DELETED_COMMENT
,
619 RevisionRecord
::DELETED_USER
,
620 RevisionRecord
::DELETED_USER
,
625 // Bitfields match, user (admin) does not have permissions
627 RevisionRecord
::DELETED_RESTRICTED
,
628 RevisionRecord
::DELETED_RESTRICTED
,
633 // Bitfields match, user (oversight) does have permissions
635 RevisionRecord
::DELETED_RESTRICTED
,
636 RevisionRecord
::DELETED_RESTRICTED
,
641 // Check permissions using the title
643 RevisionRecord
::DELETED_TEXT
,
644 RevisionRecord
::DELETED_TEXT
,
646 Title
::newFromText( __METHOD__
),
650 RevisionRecord
::DELETED_TEXT
,
651 RevisionRecord
::DELETED_TEXT
,
653 Title
::newFromText( __METHOD__
),
659 * @dataProvider provideUserCanBitfield
660 * @covers RevisionRecord::userCanBitfield
662 public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
663 $this->forceStandardPermissions();
665 $user = $this->getTestUser( $userGroups )->getUser();
669 RevisionRecord
::userCanBitfield( $bitField, $field, $user, $title )
673 public function testHasSameContent() {
677 public function testIsDeleted() {
681 public function testUserCan() {