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 $rowOverrides
27 * @return RevisionStoreRecord
29 public function newRevision( array $rowOverrides = [] ) {
30 $title = Title
::newFromText( 'Dummy' );
31 $title->resetArticleID( 17 );
33 $user = new UserIdentityValue( 11, 'Tester', 0 );
34 $comment = CommentStoreComment
::newUnsavedComment( 'Hello World' );
36 $main = SlotRecord
::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
37 $aux = SlotRecord
::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
38 $slots = new RevisionSlots( [ $main, $aux ] );
42 'rev_page' => strval( $title->getArticleID() ),
43 'rev_timestamp' => '20200101000000',
45 'rev_minor_edit' => 0,
46 'rev_parent_id' => '5',
47 'rev_len' => $slots->computeSize(),
48 'rev_sha1' => $slots->computeSha1(),
49 'page_latest' => '18',
52 $row = array_merge( $row, $rowOverrides );
54 return new RevisionStoreRecord( $title, $user, $comment, (object)$row, $slots );
57 public function provideConstructor() {
58 $title = Title
::newFromText( 'Dummy' );
59 $title->resetArticleID( 17 );
61 $user = new UserIdentityValue( 11, 'Tester', 0 );
62 $comment = CommentStoreComment
::newUnsavedComment( 'Hello World' );
64 $main = SlotRecord
::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
65 $aux = SlotRecord
::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
66 $slots = new RevisionSlots( [ $main, $aux ] );
70 'rev_page' => strval( $title->getArticleID() ),
71 'rev_timestamp' => '20200101000000',
73 'rev_minor_edit' => 0,
74 'rev_parent_id' => '5',
75 'rev_len' => $slots->computeSize(),
76 'rev_sha1' => $slots->computeSha1(),
77 'page_latest' => '18',
91 $row['rev_minor_edit'] = '1';
92 $row['rev_deleted'] = strval( RevisionRecord
::DELETED_USER
);
94 yield
'minor deleted' => [
103 $row['page_latest'] = $row['rev_id'];
114 unset( $row['rev_parent'] );
116 yield
'no parent' => [
125 unset( $row['rev_len'] );
126 unset( $row['rev_sha1'] );
128 yield
'no length, no hash' => [
137 yield
'no length, no hash' => [
138 Title
::newFromText( 'DummyDoesNotExist' ),
147 * @dataProvider provideConstructor
149 * @param Title $title
150 * @param UserIdentity $user
151 * @param CommentStoreComment $comment
153 * @param RevisionSlots $slots
154 * @param bool $wikiId
156 public function testConstructorAndGetters(
159 CommentStoreComment
$comment,
161 RevisionSlots
$slots,
164 $rec = new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
166 $this->assertSame( $title, $rec->getPageAsLinkTarget(), 'getPageAsLinkTarget' );
167 $this->assertSame( $user, $rec->getUser( RevisionRecord
::RAW
), 'getUser' );
168 $this->assertSame( $comment, $rec->getComment(), 'getComment' );
170 $this->assertSame( $slots->getSlotRoles(), $rec->getSlotRoles(), 'getSlotRoles' );
171 $this->assertSame( $wikiId, $rec->getWikiId(), 'getWikiId' );
173 $this->assertSame( (int)$row->rev_id
, $rec->getId(), 'getId' );
174 $this->assertSame( (int)$row->rev_page
, $rec->getPageId(), 'getId' );
175 $this->assertSame( $row->rev_timestamp
, $rec->getTimestamp(), 'getTimestamp' );
176 $this->assertSame( (int)$row->rev_deleted
, $rec->getVisibility(), 'getVisibility' );
177 $this->assertSame( (bool)$row->rev_minor_edit
, $rec->isMinor(), 'getIsMinor' );
179 if ( isset( $row->rev_parent_id
) ) {
180 $this->assertSame( (int)$row->rev_parent_id
, $rec->getParentId(), 'getParentId' );
182 $this->assertSame( 0, $rec->getParentId(), 'getParentId' );
185 if ( isset( $row->rev_len
) ) {
186 $this->assertSame( (int)$row->rev_len
, $rec->getSize(), 'getSize' );
188 $this->assertSame( $slots->computeSize(), $rec->getSize(), 'getSize' );
191 if ( isset( $row->rev_sha1
) ) {
192 $this->assertSame( $row->rev_sha1
, $rec->getSha1(), 'getSha1' );
194 $this->assertSame( $slots->computeSha1(), $rec->getSha1(), 'getSha1' );
197 if ( isset( $row->page_latest
) ) {
199 (int)$row->rev_id
=== (int)$row->page_latest
,
212 public function provideConstructorFailure() {
213 $title = Title
::newFromText( 'Dummy' );
214 $title->resetArticleID( 17 );
216 $user = new UserIdentityValue( 11, 'Tester', 0 );
218 $comment = CommentStoreComment
::newUnsavedComment( 'Hello World' );
220 $main = SlotRecord
::newUnsaved( 'main', new TextContent( 'Lorem Ipsum' ) );
221 $aux = SlotRecord
::newUnsaved( 'aux', new TextContent( 'Frumious Bandersnatch' ) );
222 $slots = new RevisionSlots( [ $main, $aux ] );
226 'rev_page' => strval( $title->getArticleID() ),
227 'rev_timestamp' => '20200101000000',
229 'rev_minor_edit' => 0,
230 'rev_parent_id' => '5',
231 'rev_len' => $slots->computeSize(),
232 'rev_sha1' => $slots->computeSha1(),
233 'page_latest' => '18',
236 yield
'not a row' => [
246 $row['rev_timestamp'] = 'kittens';
248 yield
'bad timestamp' => [
257 $row['rev_page'] = 99;
259 yield
'page ID mismatch' => [
269 yield
'bad wiki' => [
280 * @dataProvider provideConstructorFailure
282 * @param Title $title
283 * @param UserIdentity $user
284 * @param CommentStoreComment $comment
286 * @param RevisionSlots $slots
287 * @param bool $wikiId
289 public function testConstructorFailure(
292 CommentStoreComment
$comment,
294 RevisionSlots
$slots,
297 $this->setExpectedException( InvalidArgumentException
::class );
298 new RevisionStoreRecord( $title, $user, $comment, $row, $slots, $wikiId );
301 private function provideAudienceCheckData( $field ) {
302 yield
'field accessible for oversighter (ALL)' => [
303 RevisionRecord
::SUPPRESSED_ALL
,
309 yield
'field accessible for oversighter' => [
310 RevisionRecord
::DELETED_RESTRICTED |
$field,
316 yield
'field not accessible for sysops (ALL)' => [
317 RevisionRecord
::SUPPRESSED_ALL
,
323 yield
'field not accessible for sysops' => [
324 RevisionRecord
::DELETED_RESTRICTED |
$field,
330 yield
'field accessible for sysops' => [
337 yield
'field suppressed for logged in users' => [
344 yield
'unrelated field suppressed' => [
345 $field === RevisionRecord
::DELETED_COMMENT
346 ? RevisionRecord
::DELETED_USER
347 : RevisionRecord
::DELETED_COMMENT
,
353 yield
'nothing suppressed' => [
361 public function testSerialization_fails() {
362 $this->setExpectedException( LogicException
::class );
363 $rev = $this->newRevision();
367 public function provideGetComment_audience() {
368 return $this->provideAudienceCheckData( RevisionRecord
::DELETED_COMMENT
);
371 private function forceStandardPermissions() {
373 'wgGroupPermissions',
376 'viewsuppressed' => false,
377 'suppressrevision' => false,
378 'deletedtext' => false,
379 'deletedhistory' => false,
382 'viewsuppressed' => false,
383 'suppressrevision' => false,
384 'deletedtext' => true,
385 'deletedhistory' => true,
388 'deletedtext' => true,
389 'deletedhistory' => true,
390 'viewsuppressed' => true,
391 'suppressrevision' => true,
398 * @dataProvider provideGetComment_audience
400 public function testGetComment_audience( $visibility, $groups, $userCan, $publicCan ) {
401 $this->forceStandardPermissions();
403 $user = $this->getTestUser( $groups )->getUser();
404 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
406 $this->assertNotNull( $rev->getComment( RevisionRecord
::RAW
), 'raw can' );
410 $rev->getComment( RevisionRecord
::FOR_PUBLIC
) !== null,
415 $rev->getComment( RevisionRecord
::FOR_THIS_USER
, $user ) !== null,
420 public function provideGetUser_audience() {
421 return $this->provideAudienceCheckData( RevisionRecord
::DELETED_USER
);
425 * @dataProvider provideGetUser_audience
427 public function testGetUser_audience( $visibility, $groups, $userCan, $publicCan ) {
428 $this->forceStandardPermissions();
430 $user = $this->getTestUser( $groups )->getUser();
431 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
433 $this->assertNotNull( $rev->getUser( RevisionRecord
::RAW
), 'raw can' );
437 $rev->getUser( RevisionRecord
::FOR_PUBLIC
) !== null,
442 $rev->getUser( RevisionRecord
::FOR_THIS_USER
, $user ) !== null,
447 public function provideGetSlot_audience() {
448 return $this->provideAudienceCheckData( RevisionRecord
::DELETED_TEXT
);
452 * @dataProvider provideGetSlot_audience
454 public function testGetSlot_audience( $visibility, $groups, $userCan, $publicCan ) {
455 $this->forceStandardPermissions();
457 $user = $this->getTestUser( $groups )->getUser();
458 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
460 // NOTE: slot meta-data is never suppressed, just the content is!
461 $this->assertTrue( $rev->hasSlot( 'main' ), 'hasSlot is never suppressed' );
462 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord
::RAW
), 'raw meta' );
463 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord
::FOR_PUBLIC
), 'public meta' );
465 $this->assertNotNull(
466 $rev->getSlot( 'main', RevisionRecord
::FOR_THIS_USER
, $user ),
471 $rev->getSlot( 'main', RevisionRecord
::FOR_PUBLIC
)->getContent();
473 } catch ( SuppressedDataException
$ex ) {
484 $rev->getSlot( 'main', RevisionRecord
::FOR_THIS_USER
, $user )->getContent();
486 } catch ( SuppressedDataException
$ex ) {
497 public function provideGetSlot_audience_latest() {
498 return $this->provideAudienceCheckData( RevisionRecord
::DELETED_TEXT
);
502 * @dataProvider provideGetSlot_audience_latest
504 public function testGetSlot_audience_latest( $visibility, $groups, $userCan, $publicCan ) {
505 $this->forceStandardPermissions();
507 $user = $this->getTestUser( $groups )->getUser();
508 $rev = $this->newRevision(
510 'rev_deleted' => $visibility,
512 'page_latest' => 11, // revision is current
517 $this->assertTrue( $rev->isCurrent(), 'isCurrent()' );
519 // NOTE: slot meta-data is never suppressed, just the content is!
520 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord
::RAW
), 'raw can' );
521 $this->assertNotNull( $rev->getSlot( 'main', RevisionRecord
::FOR_PUBLIC
), 'public can' );
523 $this->assertNotNull(
524 $rev->getSlot( 'main', RevisionRecord
::FOR_THIS_USER
, $user ),
528 // NOTE: the content of the current revision is never suppressed!
529 // Check that getContent() doesn't throw SuppressedDataException
530 $rev->getSlot( 'main', RevisionRecord
::RAW
)->getContent();
531 $rev->getSlot( 'main', RevisionRecord
::FOR_PUBLIC
)->getContent();
532 $rev->getSlot( 'main', RevisionRecord
::FOR_THIS_USER
, $user )->getContent();
536 * @dataProvider provideGetSlot_audience
538 public function testGetContent_audience( $visibility, $groups, $userCan, $publicCan ) {
539 $this->forceStandardPermissions();
541 $user = $this->getTestUser( $groups )->getUser();
542 $rev = $this->newRevision( [ 'rev_deleted' => $visibility ] );
544 $this->assertNotNull( $rev->getContent( 'main', RevisionRecord
::RAW
), 'raw can' );
548 $rev->getContent( 'main', RevisionRecord
::FOR_PUBLIC
) !== null,
553 $rev->getContent( 'main', RevisionRecord
::FOR_THIS_USER
, $user ) !== null,
558 public function testGetSlot() {
559 $rev = $this->newRevision();
561 $slot = $rev->getSlot( 'main' );
562 $this->assertNotNull( $slot, 'getSlot()' );
563 $this->assertSame( 'main', $slot->getRole(), 'getRole()' );
566 public function testHasSlot() {
567 $rev = $this->newRevision();
569 $this->assertTrue( $rev->hasSlot( 'main' ) );
570 $this->assertFalse( $rev->hasSlot( 'xyz' ) );
573 public function testGetContent() {
574 $rev = $this->newRevision();
576 $content = $rev->getSlot( 'main' );
577 $this->assertNotNull( $content, 'getContent()' );
578 $this->assertSame( CONTENT_MODEL_TEXT
, $content->getModel(), 'getModel()' );
581 public function provideUserCanBitfield() {
582 yield
[ 0, 0, [], null, true ];
583 // Bitfields match, user has no permissions
585 RevisionRecord
::DELETED_TEXT
,
586 RevisionRecord
::DELETED_TEXT
,
592 RevisionRecord
::DELETED_COMMENT
,
593 RevisionRecord
::DELETED_COMMENT
,
599 RevisionRecord
::DELETED_USER
,
600 RevisionRecord
::DELETED_USER
,
606 RevisionRecord
::DELETED_RESTRICTED
,
607 RevisionRecord
::DELETED_RESTRICTED
,
612 // Bitfields match, user (admin) does have permissions
614 RevisionRecord
::DELETED_TEXT
,
615 RevisionRecord
::DELETED_TEXT
,
621 RevisionRecord
::DELETED_COMMENT
,
622 RevisionRecord
::DELETED_COMMENT
,
628 RevisionRecord
::DELETED_USER
,
629 RevisionRecord
::DELETED_USER
,
634 // Bitfields match, user (admin) does not have permissions
636 RevisionRecord
::DELETED_RESTRICTED
,
637 RevisionRecord
::DELETED_RESTRICTED
,
642 // Bitfields match, user (oversight) does have permissions
644 RevisionRecord
::DELETED_RESTRICTED
,
645 RevisionRecord
::DELETED_RESTRICTED
,
650 // Check permissions using the title
652 RevisionRecord
::DELETED_TEXT
,
653 RevisionRecord
::DELETED_TEXT
,
655 Title
::newFromText( __METHOD__
),
659 RevisionRecord
::DELETED_TEXT
,
660 RevisionRecord
::DELETED_TEXT
,
662 Title
::newFromText( __METHOD__
),
668 * @dataProvider provideUserCanBitfield
669 * @covers \MediaWiki\Storage\RevisionRecord::userCanBitfield
671 public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
672 $this->forceStandardPermissions();
674 $user = $this->getTestUser( $userGroups )->getUser();
678 RevisionRecord
::userCanBitfield( $bitField, $field, $user, $title )
682 public function provideHasSameContent() {
684 * @param SlotRecord[] $slots
686 * @return RevisionStoreRecord
688 $recordCreator = function ( array $slots, $revId ) {
689 $title = Title
::newFromText( 'provideHasSameContent' );
690 $title->resetArticleID( 19 );
691 $slots = new RevisionSlots( $slots );
693 return new RevisionStoreRecord(
695 new UserIdentityValue( 11, __METHOD__
, 0 ),
696 CommentStoreComment
::newUnsavedComment( __METHOD__
),
698 'rev_id' => strval( $revId ),
699 'rev_page' => strval( $title->getArticleID() ),
700 'rev_timestamp' => '20200101000000',
702 'rev_minor_edit' => 0,
703 'rev_parent_id' => '5',
704 'rev_len' => $slots->computeSize(),
705 'rev_sha1' => $slots->computeSha1(),
706 'page_latest' => '18',
712 // Create some slots with content
713 $mainA = SlotRecord
::newUnsaved( 'main', new TextContent( 'A' ) );
714 $mainB = SlotRecord
::newUnsaved( 'main', new TextContent( 'B' ) );
715 $auxA = SlotRecord
::newUnsaved( 'aux', new TextContent( 'A' ) );
716 $auxB = SlotRecord
::newUnsaved( 'aux', new TextContent( 'A' ) );
718 $initialRecord = $recordCreator( [ $mainA ], 12 );
721 'same record object' => [
726 'same record content, different object' => [
728 $recordCreator( [ $mainA ], 12 ),
729 $recordCreator( [ $mainA ], 13 ),
731 'same record content, aux slot, different object' => [
733 $recordCreator( [ $auxA ], 12 ),
734 $recordCreator( [ $auxB ], 13 ),
736 'different content' => [
738 $recordCreator( [ $mainA ], 12 ),
739 $recordCreator( [ $mainB ], 13 ),
741 'different content and number of slots' => [
743 $recordCreator( [ $mainA ], 12 ),
744 $recordCreator( [ $mainA, $mainB ], 13 ),
750 * @dataProvider provideHasSameContent
751 * @covers \MediaWiki\Storage\RevisionRecord::hasSameContent
754 public function testHasSameContent(
756 RevisionRecord
$record1,
757 RevisionRecord
$record2
761 $record1->hasSameContent( $record2 )
765 public function provideIsDeleted() {
766 yield
'no deletion' => [
769 RevisionRecord
::DELETED_TEXT
=> false,
770 RevisionRecord
::DELETED_COMMENT
=> false,
771 RevisionRecord
::DELETED_USER
=> false,
772 RevisionRecord
::DELETED_RESTRICTED
=> false,
775 yield
'text deleted' => [
776 RevisionRecord
::DELETED_TEXT
,
778 RevisionRecord
::DELETED_TEXT
=> true,
779 RevisionRecord
::DELETED_COMMENT
=> false,
780 RevisionRecord
::DELETED_USER
=> false,
781 RevisionRecord
::DELETED_RESTRICTED
=> false,
784 yield
'text and comment deleted' => [
785 RevisionRecord
::DELETED_TEXT + RevisionRecord
::DELETED_COMMENT
,
787 RevisionRecord
::DELETED_TEXT
=> true,
788 RevisionRecord
::DELETED_COMMENT
=> true,
789 RevisionRecord
::DELETED_USER
=> false,
790 RevisionRecord
::DELETED_RESTRICTED
=> false,
793 yield
'all 4 deleted' => [
794 RevisionRecord
::DELETED_TEXT +
795 RevisionRecord
::DELETED_COMMENT +
796 RevisionRecord
::DELETED_RESTRICTED +
797 RevisionRecord
::DELETED_USER
,
799 RevisionRecord
::DELETED_TEXT
=> true,
800 RevisionRecord
::DELETED_COMMENT
=> true,
801 RevisionRecord
::DELETED_USER
=> true,
802 RevisionRecord
::DELETED_RESTRICTED
=> true,
808 * @dataProvider provideIsDeleted
809 * @covers \MediaWiki\Storage\RevisionRecord::isDeleted
811 public function testIsDeleted( $revDeleted, $assertionMap ) {
812 $rev = $this->newRevision( [ 'rev_deleted' => $revDeleted ] );
813 foreach ( $assertionMap as $deletionLevel => $expected ) {
814 $this->assertSame( $expected, $rev->isDeleted( $deletionLevel ) );