Merge "Exclude redirects from Special:Fewestrevisions"
[lhc/web/wiklou.git] / tests / phpunit / includes / RevisionDbTestBase.php
1 <?php
2
3 use MediaWiki\MediaWikiServices;
4 use MediaWiki\Revision\MutableRevisionRecord;
5 use MediaWiki\Revision\RevisionStore;
6 use MediaWiki\Revision\IncompleteRevisionException;
7 use MediaWiki\Revision\RevisionRecord;
8 use MediaWiki\Revision\SlotRecord;
9
10 /**
11 * RevisionDbTestBase contains test cases for the Revision class that have Database interactions.
12 *
13 * @group Database
14 * @group medium
15 */
16 abstract class RevisionDbTestBase extends MediaWikiTestCase {
17
18 /**
19 * @var WikiPage $testPage
20 */
21 private $testPage;
22
23 public function __construct( $name = null, array $data = [], $dataName = '' ) {
24 parent::__construct( $name, $data, $dataName );
25
26 $this->tablesUsed = array_merge( $this->tablesUsed,
27 [
28 'page',
29 'revision',
30 'comment',
31 'ip_changes',
32 'text',
33 'archive',
34
35 'recentchanges',
36 'logging',
37
38 'page_props',
39 'pagelinks',
40 'categorylinks',
41 'langlinks',
42 'externallinks',
43 'imagelinks',
44 'templatelinks',
45 'iwlinks'
46 ]
47 );
48 }
49
50 protected function addCoreDBData() {
51 // Blank out. This would fail with a modified schema, and we don't need it.
52 }
53
54 /**
55 * @return int
56 */
57 abstract protected function getMcrMigrationStage();
58
59 /**
60 * @return string[]
61 */
62 abstract protected function getMcrTablesToReset();
63
64 protected function setUp() {
65 $this->tablesUsed += $this->getMcrTablesToReset();
66
67 parent::setUp();
68
69 $this->mergeMwGlobalArrayValue(
70 'wgExtraNamespaces',
71 [
72 12312 => 'Dummy',
73 12313 => 'Dummy_talk',
74 ]
75 );
76
77 $this->mergeMwGlobalArrayValue(
78 'wgNamespaceContentModels',
79 [
80 12312 => DummyContentForTesting::MODEL_ID,
81 ]
82 );
83
84 $this->mergeMwGlobalArrayValue(
85 'wgContentHandlers',
86 [
87 DummyContentForTesting::MODEL_ID => 'DummyContentHandlerForTesting',
88 RevisionTestModifyableContent::MODEL_ID => 'RevisionTestModifyableContentHandler',
89 ]
90 );
91
92 $this->setMwGlobals( [
93 'wgMultiContentRevisionSchemaMigrationStage' => $this->getMcrMigrationStage(),
94 'wgContentHandlerUseDB' => $this->getContentHandlerUseDB(),
95 'wgActorTableSchemaMigrationStage' => SCHEMA_COMPAT_NEW,
96 ] );
97
98 $this->overrideMwServices();
99
100 if ( !$this->testPage ) {
101 /**
102 * We have to create a new page for each subclass as the page creation may result
103 * in different DB fields being filled based on configuration.
104 */
105 $this->testPage = $this->createPage( __CLASS__, __CLASS__ );
106 }
107 }
108
109 /**
110 * @param string $model
111 * @return Title
112 */
113 protected function getMockTitle() {
114 $mock = $this->getMockBuilder( Title::class )
115 ->disableOriginalConstructor()
116 ->getMock();
117 $mock->expects( $this->any() )
118 ->method( 'getNamespace' )
119 ->will( $this->returnValue( $this->getDefaultWikitextNS() ) );
120 $mock->expects( $this->any() )
121 ->method( 'getPrefixedText' )
122 ->will( $this->returnValue( __CLASS__ ) );
123 $mock->expects( $this->any() )
124 ->method( 'getDBkey' )
125 ->will( $this->returnValue( __CLASS__ ) );
126 $mock->expects( $this->any() )
127 ->method( 'getArticleID' )
128 ->will( $this->returnValue( 23 ) );
129
130 return $mock;
131 }
132
133 abstract protected function getContentHandlerUseDB();
134
135 private function makeRevisionWithProps( $props = null ) {
136 if ( $props === null ) {
137 $props = [];
138 }
139
140 if ( !isset( $props['content'] ) && !isset( $props['text'] ) ) {
141 $props['text'] = 'Lorem Ipsum';
142 }
143
144 if ( !isset( $props['user_text'] ) ) {
145 $user = $this->getTestUser()->getUser();
146 $props['user_text'] = $user->getName();
147 $props['user'] = $user->getId();
148 }
149
150 if ( !isset( $props['user'] ) ) {
151 $props['user'] = 0;
152 }
153
154 if ( !isset( $props['comment'] ) ) {
155 $props['comment'] = 'just a test';
156 }
157
158 if ( !isset( $props['page'] ) ) {
159 $props['page'] = $this->testPage->getId();
160 }
161
162 if ( !isset( $props['content_model'] ) ) {
163 $props['content_model'] = CONTENT_MODEL_WIKITEXT;
164 }
165
166 $rev = new Revision( $props );
167
168 $dbw = wfGetDB( DB_MASTER );
169 $rev->insertOn( $dbw );
170
171 return $rev;
172 }
173
174 /**
175 * @param string $titleString
176 * @param string $text
177 * @param string|null $model
178 *
179 * @return WikiPage
180 */
181 private function createPage( $titleString, $text, $model = null ) {
182 if ( !preg_match( '/:/', $titleString ) &&
183 ( $model === null || $model === CONTENT_MODEL_WIKITEXT )
184 ) {
185 $ns = $this->getDefaultWikitextNS();
186 $titleString = MediaWikiServices::getInstance()->getNamespaceInfo()->
187 getCanonicalName( $ns ) . ':' . $titleString;
188 }
189
190 $title = Title::newFromText( $titleString );
191 $wikipage = new WikiPage( $title );
192
193 // Delete the article if it already exists
194 if ( $wikipage->exists() ) {
195 $wikipage->doDeleteArticle( "done" );
196 }
197
198 $content = ContentHandler::makeContent( $text, $title, $model );
199 $wikipage->doEditContent( $content, __METHOD__, EDIT_NEW );
200
201 return $wikipage;
202 }
203
204 private function assertRevEquals( Revision $orig, Revision $rev = null ) {
205 $this->assertNotNull( $rev, 'missing revision' );
206
207 $this->assertEquals( $orig->getId(), $rev->getId() );
208 $this->assertEquals( $orig->getPage(), $rev->getPage() );
209 $this->assertEquals( $orig->getTimestamp(), $rev->getTimestamp() );
210 $this->assertEquals( $orig->getUser(), $rev->getUser() );
211 $this->assertEquals( $orig->getContentModel(), $rev->getContentModel() );
212 $this->assertEquals( $orig->getContentFormat(), $rev->getContentFormat() );
213 $this->assertEquals( $orig->getSha1(), $rev->getSha1() );
214 }
215
216 /**
217 * @covers Revision::getRecentChange
218 */
219 public function testGetRecentChange() {
220 $rev = $this->testPage->getRevision();
221 $recentChange = $rev->getRecentChange();
222
223 // Make sure various attributes look right / the correct entry has been retrieved.
224 $this->assertEquals( $rev->getTimestamp(), $recentChange->getAttribute( 'rc_timestamp' ) );
225 $this->assertEquals(
226 $rev->getTitle()->getNamespace(),
227 $recentChange->getAttribute( 'rc_namespace' )
228 );
229 $this->assertEquals(
230 $rev->getTitle()->getDBkey(),
231 $recentChange->getAttribute( 'rc_title' )
232 );
233 $this->assertEquals( $rev->getUser(), $recentChange->getAttribute( 'rc_user' ) );
234 $this->assertEquals( $rev->getUserText(), $recentChange->getAttribute( 'rc_user_text' ) );
235 $this->assertEquals( $rev->getComment(), $recentChange->getAttribute( 'rc_comment' ) );
236 $this->assertEquals( $rev->getPage(), $recentChange->getAttribute( 'rc_cur_id' ) );
237 $this->assertEquals( $rev->getId(), $recentChange->getAttribute( 'rc_this_oldid' ) );
238 }
239
240 /**
241 * @covers Revision::insertOn
242 */
243 public function testInsertOn_success() {
244 $parentId = $this->testPage->getLatest();
245
246 // If an ExternalStore is set don't use it.
247 $this->setMwGlobals( 'wgDefaultExternalStore', false );
248
249 $rev = new Revision( [
250 'page' => $this->testPage->getId(),
251 'title' => $this->testPage->getTitle(),
252 'text' => 'Revision Text',
253 'comment' => 'Revision comment',
254 ] );
255
256 $revId = $rev->insertOn( wfGetDB( DB_MASTER ) );
257
258 $this->assertInternalType( 'integer', $revId );
259 $this->assertSame( $revId, $rev->getId() );
260
261 // getTextId() must be an int!
262 $this->assertInternalType( 'integer', $rev->getTextId() );
263
264 $mainSlot = $rev->getRevisionRecord()->getSlot( SlotRecord::MAIN, RevisionRecord::RAW );
265
266 // we currently only support storage in the text table
267 $textId = MediaWikiServices::getInstance()
268 ->getBlobStore()
269 ->getTextIdFromAddress( $mainSlot->getAddress() );
270
271 $this->assertSelect(
272 'text',
273 [ 'old_id', 'old_text' ],
274 "old_id = $textId",
275 [ [ strval( $textId ), 'Revision Text' ] ]
276 );
277 $this->assertSelect(
278 'revision',
279 [
280 'rev_id',
281 'rev_page',
282 'rev_minor_edit',
283 'rev_deleted',
284 'rev_len',
285 'rev_parent_id',
286 'rev_sha1',
287 ],
288 "rev_id = {$rev->getId()}",
289 [ [
290 strval( $rev->getId() ),
291 strval( $this->testPage->getId() ),
292 '0',
293 '0',
294 '13',
295 strval( $parentId ),
296 's0ngbdoxagreuf2vjtuxzwdz64n29xm',
297 ] ]
298 );
299 }
300
301 public function provideInsertOn_exceptionOnIncomplete() {
302 $content = new TextContent( '' );
303 $user = User::newFromName( 'Foo' );
304
305 yield 'no parent' => [
306 [
307 'content' => $content,
308 'comment' => 'test',
309 'user' => $user,
310 ],
311 IncompleteRevisionException::class,
312 "rev_page field must not be 0!"
313 ];
314
315 yield 'no comment' => [
316 [
317 'content' => $content,
318 'page' => 7,
319 'user' => $user,
320 ],
321 IncompleteRevisionException::class,
322 "comment must not be NULL!"
323 ];
324
325 yield 'no content' => [
326 [
327 'comment' => 'test',
328 'page' => 7,
329 'user' => $user,
330 ],
331 IncompleteRevisionException::class,
332 "Uninitialized field: content_address" // XXX: message may change
333 ];
334 }
335
336 /**
337 * @dataProvider provideInsertOn_exceptionOnIncomplete
338 * @covers Revision::insertOn
339 */
340 public function testInsertOn_exceptionOnIncomplete( $array, $expException, $expMessage ) {
341 // If an ExternalStore is set don't use it.
342 $this->setMwGlobals( 'wgDefaultExternalStore', false );
343 $this->setExpectedException( $expException, $expMessage );
344
345 $title = Title::newFromText( 'Nonexistant-' . __METHOD__ );
346 $rev = new Revision( $array, 0, $title );
347
348 $rev->insertOn( wfGetDB( DB_MASTER ) );
349 }
350
351 /**
352 * @covers Revision::newFromTitle
353 */
354 public function testNewFromTitle_withoutId() {
355 $latestRevId = $this->testPage->getLatest();
356
357 $rev = Revision::newFromTitle( $this->testPage->getTitle() );
358
359 $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) );
360 $this->assertEquals( $latestRevId, $rev->getId() );
361 }
362
363 /**
364 * @covers Revision::newFromTitle
365 */
366 public function testNewFromTitle_withId() {
367 $latestRevId = $this->testPage->getLatest();
368
369 $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId );
370
371 $this->assertTrue( $this->testPage->getTitle()->equals( $rev->getTitle() ) );
372 $this->assertEquals( $latestRevId, $rev->getId() );
373 }
374
375 /**
376 * @covers Revision::newFromTitle
377 */
378 public function testNewFromTitle_withBadId() {
379 $latestRevId = $this->testPage->getLatest();
380
381 $rev = Revision::newFromTitle( $this->testPage->getTitle(), $latestRevId + 1 );
382
383 $this->assertNull( $rev );
384 }
385
386 /**
387 * @covers Revision::newFromRow
388 */
389 public function testNewFromRow() {
390 $orig = $this->makeRevisionWithProps();
391
392 $dbr = wfGetDB( DB_REPLICA );
393 $revQuery = Revision::getQueryInfo();
394 $res = $dbr->select( $revQuery['tables'], $revQuery['fields'], [ 'rev_id' => $orig->getId() ],
395 __METHOD__, [], $revQuery['joins'] );
396 $this->assertTrue( is_object( $res ), 'query failed' );
397
398 $row = $res->fetchObject();
399 $res->free();
400
401 $rev = Revision::newFromRow( $row );
402
403 $this->assertRevEquals( $orig, $rev );
404 }
405
406 public function provideNewFromArchiveRow() {
407 yield [
408 function ( $f ) {
409 return $f;
410 },
411 ];
412 yield [
413 function ( $f ) {
414 return $f + [ 'ar_namespace', 'ar_title' ];
415 },
416 ];
417 yield [
418 function ( $f ) {
419 unset( $f['ar_text_id'] );
420 return $f;
421 },
422 ];
423 yield [
424 function ( $f ) {
425 unset( $f['ar_page_id'] );
426 return $f;
427 },
428 ];
429 yield [
430 function ( $f ) {
431 unset( $f['ar_parent_id'] );
432 return $f;
433 },
434 ];
435 yield [
436 function ( $f ) {
437 unset( $f['ar_rev_id'] );
438 return $f;
439 },
440 ];
441 yield [
442 function ( $f ) {
443 unset( $f['ar_sha1'] );
444 return $f;
445 },
446 ];
447 }
448
449 /**
450 * @dataProvider provideNewFromArchiveRow
451 * @covers Revision::newFromArchiveRow
452 */
453 public function testNewFromArchiveRow( $selectModifier ) {
454 $services = MediaWikiServices::getInstance();
455
456 $store = new RevisionStore(
457 $services->getDBLoadBalancer(),
458 $services->getService( '_SqlBlobStore' ),
459 $services->getMainWANObjectCache(),
460 $services->getCommentStore(),
461 $services->getContentModelStore(),
462 $services->getSlotRoleStore(),
463 $services->getSlotRoleRegistry(),
464 $this->getMcrMigrationStage(),
465 $services->getActorMigration()
466 );
467
468 $store->setContentHandlerUseDB( $this->getContentHandlerUseDB() );
469 $this->setService( 'RevisionStore', $store );
470
471 $page = $this->createPage(
472 'RevisionStorageTest_testNewFromArchiveRow',
473 'Lorem Ipsum',
474 CONTENT_MODEL_WIKITEXT
475 );
476 $orig = $page->getRevision();
477 $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
478
479 $dbr = wfGetDB( DB_REPLICA );
480 $arQuery = Revision::getArchiveQueryInfo();
481 $arQuery['fields'] = $selectModifier( $arQuery['fields'] );
482 $res = $dbr->select(
483 $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
484 __METHOD__, [], $arQuery['joins']
485 );
486 $this->assertTrue( is_object( $res ), 'query failed' );
487
488 $row = $res->fetchObject();
489 $res->free();
490
491 // MCR migration note: $row is now required to contain ar_title and ar_namespace.
492 // Alternatively, a Title object can be passed to RevisionStore::newRevisionFromArchiveRow
493 $rev = Revision::newFromArchiveRow( $row );
494
495 $this->assertRevEquals( $orig, $rev );
496 }
497
498 /**
499 * @covers Revision::newFromArchiveRow
500 */
501 public function testNewFromArchiveRowOverrides() {
502 $page = $this->createPage(
503 'RevisionStorageTest_testNewFromArchiveRow',
504 'Lorem Ipsum',
505 CONTENT_MODEL_WIKITEXT
506 );
507 $orig = $page->getRevision();
508 $page->doDeleteArticle( 'test Revision::newFromArchiveRow' );
509
510 $dbr = wfGetDB( DB_REPLICA );
511 $arQuery = Revision::getArchiveQueryInfo();
512 $res = $dbr->select(
513 $arQuery['tables'], $arQuery['fields'], [ 'ar_rev_id' => $orig->getId() ],
514 __METHOD__, [], $arQuery['joins']
515 );
516 $this->assertTrue( is_object( $res ), 'query failed' );
517
518 $row = $res->fetchObject();
519 $res->free();
520
521 $rev = Revision::newFromArchiveRow( $row, [ 'comment_text' => 'SOMEOVERRIDE' ] );
522
523 $this->assertNotEquals( $orig->getComment(), $rev->getComment() );
524 $this->assertEquals( 'SOMEOVERRIDE', $rev->getComment() );
525 }
526
527 /**
528 * @covers Revision::newFromId
529 */
530 public function testNewFromId() {
531 $orig = $this->testPage->getRevision();
532 $rev = Revision::newFromId( $orig->getId() );
533 $this->assertRevEquals( $orig, $rev );
534 }
535
536 /**
537 * @covers Revision::newFromPageId
538 */
539 public function testNewFromPageId() {
540 $rev = Revision::newFromPageId( $this->testPage->getId() );
541 $this->assertRevEquals(
542 $this->testPage->getRevision(),
543 $rev
544 );
545 }
546
547 /**
548 * @covers Revision::newFromPageId
549 */
550 public function testNewFromPageIdWithLatestId() {
551 $rev = Revision::newFromPageId(
552 $this->testPage->getId(),
553 $this->testPage->getLatest()
554 );
555 $this->assertRevEquals(
556 $this->testPage->getRevision(),
557 $rev
558 );
559 }
560
561 /**
562 * @covers Revision::newFromPageId
563 */
564 public function testNewFromPageIdWithNotLatestId() {
565 $content = new WikitextContent( __METHOD__ );
566 $this->testPage->doEditContent( $content, __METHOD__ );
567 $rev = Revision::newFromPageId(
568 $this->testPage->getId(),
569 $this->testPage->getRevision()->getPrevious()->getId()
570 );
571 $this->assertRevEquals(
572 $this->testPage->getRevision()->getPrevious(),
573 $rev
574 );
575 }
576
577 /**
578 * @covers Revision::getPage
579 */
580 public function testGetPage() {
581 $page = $this->testPage;
582
583 $orig = $this->makeRevisionWithProps( [ 'page' => $page->getId() ] );
584 $rev = Revision::newFromId( $orig->getId() );
585
586 $this->assertEquals( $page->getId(), $rev->getPage() );
587 }
588
589 /**
590 * @covers Revision::isCurrent
591 */
592 public function testIsCurrent() {
593 $rev1 = $this->testPage->getRevision();
594
595 # @todo find out if this should be true
596 # $this->assertTrue( $rev1->isCurrent() );
597
598 $rev1x = Revision::newFromId( $rev1->getId() );
599 $this->assertTrue( $rev1x->isCurrent() );
600
601 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
602 $rev2 = $this->testPage->getRevision();
603
604 # @todo find out if this should be true
605 # $this->assertTrue( $rev2->isCurrent() );
606
607 $rev1x = Revision::newFromId( $rev1->getId() );
608 $this->assertFalse( $rev1x->isCurrent() );
609
610 $rev2x = Revision::newFromId( $rev2->getId() );
611 $this->assertTrue( $rev2x->isCurrent() );
612 }
613
614 /**
615 * @covers Revision::getPrevious
616 */
617 public function testGetPrevious() {
618 $oldestRevision = $this->testPage->getOldestRevision();
619 $latestRevision = $this->testPage->getLatest();
620
621 $this->assertNull( $oldestRevision->getPrevious() );
622
623 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
624 $newRevision = $this->testPage->getRevision();
625
626 $this->assertNotNull( $newRevision->getPrevious() );
627 $this->assertEquals( $latestRevision, $newRevision->getPrevious()->getId() );
628 }
629
630 /**
631 * @covers Title::getPreviousRevisionID
632 * @covers Title::getRelativeRevisionID
633 * @covers MediaWiki\Revision\RevisionStore::getPreviousRevision
634 * @covers MediaWiki\Revision\RevisionStore::getRelativeRevision
635 */
636 public function testTitleGetPreviousRevisionID() {
637 $oldestId = $this->testPage->getOldestRevision()->getId();
638 $latestId = $this->testPage->getLatest();
639
640 $title = $this->testPage->getTitle();
641
642 $this->assertFalse( $title->getPreviousRevisionID( $oldestId ) );
643
644 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
645 $newId = $this->testPage->getRevision()->getId();
646
647 $this->assertEquals( $latestId, $title->getPreviousRevisionID( $newId ) );
648 }
649
650 /**
651 * @covers Title::getPreviousRevisionID
652 * @covers Title::getRelativeRevisionID
653 */
654 public function testTitleGetPreviousRevisionID_invalid() {
655 $this->assertFalse( $this->testPage->getTitle()->getPreviousRevisionID( 123456789 ) );
656 }
657
658 /**
659 * @covers Revision::getNext
660 */
661 public function testGetNext() {
662 $rev1 = $this->testPage->getRevision();
663
664 $this->assertNull( $rev1->getNext() );
665
666 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
667 $rev2 = $this->testPage->getRevision();
668
669 $this->assertNotNull( $rev1->getNext() );
670 $this->assertEquals( $rev2->getId(), $rev1->getNext()->getId() );
671 }
672
673 /**
674 * @covers Title::getNextRevisionID
675 * @covers Title::getRelativeRevisionID
676 * @covers MediaWiki\Revision\RevisionStore::getNextRevision
677 * @covers MediaWiki\Revision\RevisionStore::getRelativeRevision
678 */
679 public function testTitleGetNextRevisionID() {
680 $title = $this->testPage->getTitle();
681
682 $origId = $this->testPage->getLatest();
683
684 $this->assertFalse( $title->getNextRevisionID( $origId ) );
685
686 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
687 $newId = $this->testPage->getLatest();
688
689 $this->assertSame( $this->testPage->getLatest(), $title->getNextRevisionID( $origId ) );
690 }
691
692 /**
693 * @covers Title::getNextRevisionID
694 * @covers Title::getRelativeRevisionID
695 */
696 public function testTitleGetNextRevisionID_invalid() {
697 $this->assertFalse( $this->testPage->getTitle()->getNextRevisionID( 123456789 ) );
698 }
699
700 /**
701 * @covers Revision::newNullRevision
702 */
703 public function testNewNullRevision() {
704 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
705 $orig = $this->testPage->getRevision();
706
707 $dbw = wfGetDB( DB_MASTER );
708 $rev = Revision::newNullRevision( $dbw, $this->testPage->getId(), 'a null revision', false );
709
710 $this->assertNotEquals( $orig->getId(), $rev->getId(),
711 'new null revision should have a different id from the original revision' );
712 $this->assertEquals( $orig->getTextId(), $rev->getTextId(),
713 'new null revision should have the same text id as the original revision' );
714 $this->assertEquals( $orig->getSha1(), $rev->getSha1(),
715 'new null revision should have the same SHA1 as the original revision' );
716 $this->assertTrue( $orig->getRevisionRecord()->hasSameContent( $rev->getRevisionRecord() ),
717 'new null revision should have the same content as the original revision' );
718 $this->assertEquals( __METHOD__, $rev->getContent()->getText() );
719 }
720
721 /**
722 * @covers Revision::newNullRevision
723 */
724 public function testNewNullRevision_badPage() {
725 $dbw = wfGetDB( DB_MASTER );
726 $rev = Revision::newNullRevision( $dbw, -1, 'a null revision', false );
727
728 $this->assertNull( $rev );
729 }
730
731 /**
732 * @covers Revision::insertOn
733 */
734 public function testInsertOn() {
735 $ip = '2600:387:ed7:947e:8c16:a1ad:dd34:1dd7';
736
737 $orig = $this->makeRevisionWithProps( [
738 'user_text' => $ip
739 ] );
740
741 // Make sure the revision was copied to ip_changes
742 $dbr = wfGetDB( DB_REPLICA );
743 $res = $dbr->select( 'ip_changes', '*', [ 'ipc_rev_id' => $orig->getId() ] );
744 $row = $res->fetchObject();
745
746 $this->assertEquals( IP::toHex( $ip ), $row->ipc_hex );
747 $this->assertEquals(
748 $orig->getTimestamp(),
749 wfTimestamp( TS_MW, $row->ipc_rev_timestamp )
750 );
751 }
752
753 public static function provideUserWasLastToEdit() {
754 yield 'actually the last edit' => [ 3, true ];
755 yield 'not the current edit, but still by this user' => [ 2, true ];
756 yield 'edit by another user' => [ 1, false ];
757 yield 'first edit, by this user, but another user edited in the mean time' => [ 0, false ];
758 }
759
760 /**
761 * @covers Revision::userWasLastToEdit
762 * @dataProvider provideUserWasLastToEdit
763 */
764 public function testUserWasLastToEdit( $sinceIdx, $expectedLast ) {
765 $userA = User::newFromName( "RevisionStorageTest_userA" );
766 $userB = User::newFromName( "RevisionStorageTest_userB" );
767
768 if ( $userA->getId() === 0 ) {
769 $userA = User::createNew( $userA->getName() );
770 }
771
772 if ( $userB->getId() === 0 ) {
773 $userB = User::createNew( $userB->getName() );
774 }
775
776 $ns = $this->getDefaultWikitextNS();
777
778 $dbw = wfGetDB( DB_MASTER );
779 $revisions = [];
780
781 // create revisions -----------------------------
782 $page = WikiPage::factory( Title::newFromText(
783 'RevisionStorageTest_testUserWasLastToEdit', $ns ) );
784 $page->insertOn( $dbw );
785
786 $revisions[0] = new Revision( [
787 'page' => $page->getId(),
788 // we need the title to determine the page's default content model
789 'title' => $page->getTitle(),
790 'timestamp' => '20120101000000',
791 'user' => $userA->getId(),
792 'text' => 'zero',
793 'content_model' => CONTENT_MODEL_WIKITEXT,
794 'comment' => 'edit zero'
795 ] );
796 $revisions[0]->insertOn( $dbw );
797
798 $revisions[1] = new Revision( [
799 'page' => $page->getId(),
800 // still need the title, because $page->getId() is 0 (there's no entry in the page table)
801 'title' => $page->getTitle(),
802 'timestamp' => '20120101000100',
803 'user' => $userA->getId(),
804 'text' => 'one',
805 'content_model' => CONTENT_MODEL_WIKITEXT,
806 'comment' => 'edit one'
807 ] );
808 $revisions[1]->insertOn( $dbw );
809
810 $revisions[2] = new Revision( [
811 'page' => $page->getId(),
812 'title' => $page->getTitle(),
813 'timestamp' => '20120101000200',
814 'user' => $userB->getId(),
815 'text' => 'two',
816 'content_model' => CONTENT_MODEL_WIKITEXT,
817 'comment' => 'edit two'
818 ] );
819 $revisions[2]->insertOn( $dbw );
820
821 $revisions[3] = new Revision( [
822 'page' => $page->getId(),
823 'title' => $page->getTitle(),
824 'timestamp' => '20120101000300',
825 'user' => $userA->getId(),
826 'text' => 'three',
827 'content_model' => CONTENT_MODEL_WIKITEXT,
828 'comment' => 'edit three'
829 ] );
830 $revisions[3]->insertOn( $dbw );
831
832 $revisions[4] = new Revision( [
833 'page' => $page->getId(),
834 'title' => $page->getTitle(),
835 'timestamp' => '20120101000200',
836 'user' => $userA->getId(),
837 'text' => 'zero',
838 'content_model' => CONTENT_MODEL_WIKITEXT,
839 'comment' => 'edit four'
840 ] );
841 $revisions[4]->insertOn( $dbw );
842
843 // test it ---------------------------------
844 $since = $revisions[$sinceIdx]->getTimestamp();
845
846 $revQuery = Revision::getQueryInfo();
847 $allRows = iterator_to_array( $dbw->select(
848 $revQuery['tables'],
849 [ 'rev_id', 'rev_timestamp', 'rev_user' => $revQuery['fields']['rev_user'] ],
850 [
851 'rev_page' => $page->getId(),
852 //'rev_timestamp > ' . $dbw->addQuotes( $dbw->timestamp( $since ) )
853 ],
854 __METHOD__,
855 [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 50 ],
856 $revQuery['joins']
857 ) );
858
859 $wasLast = Revision::userWasLastToEdit( $dbw, $page->getId(), $userA->getId(), $since );
860
861 $this->assertEquals( $expectedLast, $wasLast );
862 }
863
864 /**
865 * @param string $text
866 * @param string $title
867 * @param string $model
868 * @param string $format
869 *
870 * @return Revision
871 */
872 private function newTestRevision( $text, $title = "Test",
873 $model = CONTENT_MODEL_WIKITEXT, $format = null
874 ) {
875 if ( is_string( $title ) ) {
876 $title = Title::newFromText( $title );
877 }
878
879 $content = ContentHandler::makeContent( $text, $title, $model, $format );
880
881 $rev = new Revision(
882 [
883 'id' => 42,
884 'page' => 23,
885 'title' => $title,
886
887 'content' => $content,
888 'length' => $content->getSize(),
889 'comment' => "testing",
890 'minor_edit' => false,
891
892 'content_format' => $format,
893 ]
894 );
895
896 return $rev;
897 }
898
899 public function provideGetContentModel() {
900 // NOTE: we expect the help namespace to always contain wikitext
901 return [
902 [ 'hello world', 'Help:Hello', null, null, CONTENT_MODEL_WIKITEXT ],
903 [ 'hello world', 'User:hello/there.css', null, null, CONTENT_MODEL_CSS ],
904 [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ],
905 ];
906 }
907
908 /**
909 * @dataProvider provideGetContentModel
910 * @covers Revision::getContentModel
911 */
912 public function testGetContentModel( $text, $title, $model, $format, $expectedModel ) {
913 $rev = $this->newTestRevision( $text, $title, $model, $format );
914
915 $this->assertEquals( $expectedModel, $rev->getContentModel() );
916 }
917
918 public function provideGetContentFormat() {
919 // NOTE: we expect the help namespace to always contain wikitext
920 return [
921 [ 'hello world', 'Help:Hello', null, null, CONTENT_FORMAT_WIKITEXT ],
922 [ 'hello world', 'Help:Hello', CONTENT_MODEL_CSS, null, CONTENT_FORMAT_CSS ],
923 [ 'hello world', 'User:hello/there.css', null, null, CONTENT_FORMAT_CSS ],
924 [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentForTesting::MODEL_ID ],
925 ];
926 }
927
928 /**
929 * @dataProvider provideGetContentFormat
930 * @covers Revision::getContentFormat
931 */
932 public function testGetContentFormat( $text, $title, $model, $format, $expectedFormat ) {
933 $rev = $this->newTestRevision( $text, $title, $model, $format );
934
935 $this->assertEquals( $expectedFormat, $rev->getContentFormat() );
936 }
937
938 public function provideGetContentHandler() {
939 // NOTE: we expect the help namespace to always contain wikitext
940 return [
941 [ 'hello world', 'Help:Hello', null, null, WikitextContentHandler::class ],
942 [ 'hello world', 'User:hello/there.css', null, null, CssContentHandler::class ],
943 [ serialize( 'hello world' ), 'Dummy:Hello', null, null, DummyContentHandlerForTesting::class ],
944 ];
945 }
946
947 /**
948 * @dataProvider provideGetContentHandler
949 * @covers Revision::getContentHandler
950 */
951 public function testGetContentHandler( $text, $title, $model, $format, $expectedClass ) {
952 $rev = $this->newTestRevision( $text, $title, $model, $format );
953
954 $this->assertEquals( $expectedClass, get_class( $rev->getContentHandler() ) );
955 }
956
957 public function provideGetContent() {
958 // NOTE: we expect the help namespace to always contain wikitext
959 return [
960 [ 'hello world', 'Help:Hello', null, null, Revision::FOR_PUBLIC, 'hello world' ],
961 [
962 serialize( 'hello world' ),
963 'Hello',
964 DummyContentForTesting::MODEL_ID,
965 null,
966 Revision::FOR_PUBLIC,
967 serialize( 'hello world' )
968 ],
969 [
970 serialize( 'hello world' ),
971 'Dummy:Hello',
972 null,
973 null,
974 Revision::FOR_PUBLIC,
975 serialize( 'hello world' )
976 ],
977 ];
978 }
979
980 /**
981 * @dataProvider provideGetContent
982 * @covers Revision::getContent
983 */
984 public function testGetContent( $text, $title, $model, $format,
985 $audience, $expectedSerialization
986 ) {
987 $rev = $this->newTestRevision( $text, $title, $model, $format );
988 $content = $rev->getContent( $audience );
989
990 $this->assertEquals(
991 $expectedSerialization,
992 is_null( $content ) ? null : $content->serialize( $format )
993 );
994 }
995
996 /**
997 * @covers Revision::getContent
998 */
999 public function testGetContent_failure() {
1000 $rev = new Revision( [
1001 'page' => $this->testPage->getId(),
1002 'content_model' => $this->testPage->getContentModel(),
1003 'id' => 123456789, // not in the test DB
1004 ] );
1005
1006 Wikimedia\suppressWarnings(); // bad text_id will trigger a warning.
1007
1008 $this->assertNull( $rev->getContent(),
1009 "getContent() should return null if the revision's text blob could not be loaded." );
1010
1011 // NOTE: check this twice, once for lazy initialization, and once with the cached value.
1012 $this->assertNull( $rev->getContent(),
1013 "getContent() should return null if the revision's text blob could not be loaded." );
1014
1015 Wikimedia\restoreWarnings();
1016 }
1017
1018 public function provideGetSize() {
1019 return [
1020 [ "hello world.", CONTENT_MODEL_WIKITEXT, 12 ],
1021 [ serialize( "hello world." ), DummyContentForTesting::MODEL_ID, 12 ],
1022 ];
1023 }
1024
1025 /**
1026 * @covers Revision::getSize
1027 * @dataProvider provideGetSize
1028 */
1029 public function testGetSize( $text, $model, $expected_size ) {
1030 $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSize', $model );
1031 $this->assertEquals( $expected_size, $rev->getSize() );
1032 }
1033
1034 public function provideGetSha1() {
1035 return [
1036 [ "hello world.", CONTENT_MODEL_WIKITEXT, Revision::base36Sha1( "hello world." ) ],
1037 [
1038 serialize( "hello world." ),
1039 DummyContentForTesting::MODEL_ID,
1040 Revision::base36Sha1( serialize( "hello world." ) )
1041 ],
1042 ];
1043 }
1044
1045 /**
1046 * @covers Revision::getSha1
1047 * @dataProvider provideGetSha1
1048 */
1049 public function testGetSha1( $text, $model, $expected_hash ) {
1050 $rev = $this->newTestRevision( $text, 'RevisionTest_testGetSha1', $model );
1051 $this->assertEquals( $expected_hash, $rev->getSha1() );
1052 }
1053
1054 /**
1055 * Tests whether $rev->getContent() returns a clone when needed.
1056 *
1057 * @covers Revision::getContent
1058 */
1059 public function testGetContentClone() {
1060 $content = new RevisionTestModifyableContent( "foo" );
1061
1062 $rev = new Revision(
1063 [
1064 'id' => 42,
1065 'page' => 23,
1066 'title' => Title::newFromText( "testGetContentClone_dummy" ),
1067
1068 'content' => $content,
1069 'length' => $content->getSize(),
1070 'comment' => "testing",
1071 'minor_edit' => false,
1072 ]
1073 );
1074
1075 /** @var RevisionTestModifyableContent $content */
1076 $content = $rev->getContent( Revision::RAW );
1077 $content->setText( "bar" );
1078
1079 /** @var RevisionTestModifyableContent $content2 */
1080 $content2 = $rev->getContent( Revision::RAW );
1081 // content is mutable, expect clone
1082 $this->assertNotSame( $content, $content2, "expected a clone" );
1083 // clone should contain the original text
1084 $this->assertEquals( "foo", $content2->getText() );
1085
1086 $content2->setText( "bla bla" );
1087 // clones should be independent
1088 $this->assertEquals( "bar", $content->getText() );
1089 }
1090
1091 /**
1092 * Tests whether $rev->getContent() returns the same object repeatedly if appropriate.
1093 * @covers Revision::getContent
1094 */
1095 public function testGetContentUncloned() {
1096 $rev = $this->newTestRevision( "hello", "testGetContentUncloned_dummy", CONTENT_MODEL_WIKITEXT );
1097 $content = $rev->getContent( Revision::RAW );
1098 $content2 = $rev->getContent( Revision::RAW );
1099
1100 // for immutable content like wikitext, this should be the same object
1101 $this->assertSame( $content, $content2 );
1102 }
1103
1104 /**
1105 * @covers Revision::loadFromId
1106 */
1107 public function testLoadFromId() {
1108 $rev = $this->testPage->getRevision();
1109 $this->hideDeprecated( 'Revision::loadFromId' );
1110 $this->assertRevEquals(
1111 $rev,
1112 Revision::loadFromId( wfGetDB( DB_MASTER ), $rev->getId() )
1113 );
1114 }
1115
1116 /**
1117 * @covers Revision::loadFromPageId
1118 */
1119 public function testLoadFromPageId() {
1120 $this->assertRevEquals(
1121 $this->testPage->getRevision(),
1122 Revision::loadFromPageId( wfGetDB( DB_MASTER ), $this->testPage->getId() )
1123 );
1124 }
1125
1126 /**
1127 * @covers Revision::loadFromPageId
1128 */
1129 public function testLoadFromPageIdWithLatestRevId() {
1130 $this->assertRevEquals(
1131 $this->testPage->getRevision(),
1132 Revision::loadFromPageId(
1133 wfGetDB( DB_MASTER ),
1134 $this->testPage->getId(),
1135 $this->testPage->getLatest()
1136 )
1137 );
1138 }
1139
1140 /**
1141 * @covers Revision::loadFromPageId
1142 */
1143 public function testLoadFromPageIdWithNotLatestRevId() {
1144 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
1145 $this->assertRevEquals(
1146 $this->testPage->getRevision()->getPrevious(),
1147 Revision::loadFromPageId(
1148 wfGetDB( DB_MASTER ),
1149 $this->testPage->getId(),
1150 $this->testPage->getRevision()->getPrevious()->getId()
1151 )
1152 );
1153 }
1154
1155 /**
1156 * @covers Revision::loadFromTitle
1157 */
1158 public function testLoadFromTitle() {
1159 $this->assertRevEquals(
1160 $this->testPage->getRevision(),
1161 Revision::loadFromTitle( wfGetDB( DB_MASTER ), $this->testPage->getTitle() )
1162 );
1163 }
1164
1165 /**
1166 * @covers Revision::loadFromTitle
1167 */
1168 public function testLoadFromTitleWithLatestRevId() {
1169 $this->assertRevEquals(
1170 $this->testPage->getRevision(),
1171 Revision::loadFromTitle(
1172 wfGetDB( DB_MASTER ),
1173 $this->testPage->getTitle(),
1174 $this->testPage->getLatest()
1175 )
1176 );
1177 }
1178
1179 /**
1180 * @covers Revision::loadFromTitle
1181 */
1182 public function testLoadFromTitleWithNotLatestRevId() {
1183 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
1184 $this->assertRevEquals(
1185 $this->testPage->getRevision()->getPrevious(),
1186 Revision::loadFromTitle(
1187 wfGetDB( DB_MASTER ),
1188 $this->testPage->getTitle(),
1189 $this->testPage->getRevision()->getPrevious()->getId()
1190 )
1191 );
1192 }
1193
1194 /**
1195 * @covers Revision::loadFromTimestamp()
1196 */
1197 public function testLoadFromTimestamp() {
1198 $this->assertRevEquals(
1199 $this->testPage->getRevision(),
1200 Revision::loadFromTimestamp(
1201 wfGetDB( DB_MASTER ),
1202 $this->testPage->getTitle(),
1203 $this->testPage->getRevision()->getTimestamp()
1204 )
1205 );
1206 }
1207
1208 /**
1209 * @covers Revision::getParentLengths
1210 */
1211 public function testGetParentLengths_noRevIds() {
1212 $this->assertSame(
1213 [],
1214 Revision::getParentLengths(
1215 wfGetDB( DB_MASTER ),
1216 []
1217 )
1218 );
1219 }
1220
1221 /**
1222 * @covers Revision::getParentLengths
1223 */
1224 public function testGetParentLengths_oneRevId() {
1225 $text = '831jr091jr0921kr21kr0921kjr0921j09rj1';
1226 $textLength = strlen( $text );
1227
1228 $this->testPage->doEditContent( new WikitextContent( $text ), __METHOD__ );
1229 $rev[1] = $this->testPage->getLatest();
1230
1231 $this->assertSame(
1232 [ $rev[1] => $textLength ],
1233 Revision::getParentLengths(
1234 wfGetDB( DB_MASTER ),
1235 [ $rev[1] ]
1236 )
1237 );
1238 }
1239
1240 /**
1241 * @covers Revision::getParentLengths
1242 */
1243 public function testGetParentLengths_multipleRevIds() {
1244 $textOne = '831jr091jr0921kr21kr0921kjr0921j09rj1';
1245 $textOneLength = strlen( $textOne );
1246 $textTwo = '831jr091jr092121j09rj1';
1247 $textTwoLength = strlen( $textTwo );
1248
1249 $this->testPage->doEditContent( new WikitextContent( $textOne ), __METHOD__ );
1250 $rev[1] = $this->testPage->getLatest();
1251 $this->testPage->doEditContent( new WikitextContent( $textTwo ), __METHOD__ );
1252 $rev[2] = $this->testPage->getLatest();
1253
1254 $this->assertSame(
1255 [ $rev[1] => $textOneLength, $rev[2] => $textTwoLength ],
1256 Revision::getParentLengths(
1257 wfGetDB( DB_MASTER ),
1258 [ $rev[1], $rev[2] ]
1259 )
1260 );
1261 }
1262
1263 /**
1264 * @covers Revision::getTitle
1265 */
1266 public function testGetTitle_fromExistingRevision() {
1267 $this->assertTrue(
1268 $this->testPage->getTitle()->equals(
1269 $this->testPage->getRevision()->getTitle()
1270 )
1271 );
1272 }
1273
1274 /**
1275 * @covers Revision::getTitle
1276 */
1277 public function testGetTitle_fromRevisionWhichWillLoadTheTitle() {
1278 $rev = new Revision( [ 'id' => $this->testPage->getLatest() ] );
1279 $this->assertTrue(
1280 $this->testPage->getTitle()->equals(
1281 $rev->getTitle()
1282 )
1283 );
1284 }
1285
1286 /**
1287 * @covers Revision::isMinor
1288 */
1289 public function testIsMinor_true() {
1290 // Use a sysop to ensure we can mark edits as minor
1291 $sysop = $this->getTestSysop()->getUser();
1292
1293 $this->testPage->doEditContent(
1294 new WikitextContent( __METHOD__ ),
1295 __METHOD__,
1296 EDIT_MINOR,
1297 false,
1298 $sysop
1299 );
1300 $rev = $this->testPage->getRevision();
1301
1302 $this->assertSame( true, $rev->isMinor() );
1303 }
1304
1305 /**
1306 * @covers Revision::isMinor
1307 */
1308 public function testIsMinor_false() {
1309 $this->testPage->doEditContent(
1310 new WikitextContent( __METHOD__ ),
1311 __METHOD__,
1312 0
1313 );
1314 $rev = $this->testPage->getRevision();
1315
1316 $this->assertSame( false, $rev->isMinor() );
1317 }
1318
1319 /**
1320 * @covers Revision::getTimestamp
1321 */
1322 public function testGetTimestamp() {
1323 $testTimestamp = wfTimestampNow();
1324
1325 $this->testPage->doEditContent(
1326 new WikitextContent( __METHOD__ ),
1327 __METHOD__
1328 );
1329 $rev = $this->testPage->getRevision();
1330
1331 $this->assertInternalType( 'string', $rev->getTimestamp() );
1332 $this->assertTrue( strlen( $rev->getTimestamp() ) == strlen( 'YYYYMMDDHHMMSS' ) );
1333 $this->assertContains( substr( $testTimestamp, 0, 10 ), $rev->getTimestamp() );
1334 }
1335
1336 /**
1337 * @covers Revision::getUser
1338 * @covers Revision::getUserText
1339 */
1340 public function testGetUserAndText() {
1341 $sysop = $this->getTestSysop()->getUser();
1342
1343 $this->testPage->doEditContent(
1344 new WikitextContent( __METHOD__ ),
1345 __METHOD__,
1346 0,
1347 false,
1348 $sysop
1349 );
1350 $rev = $this->testPage->getRevision();
1351
1352 $this->assertSame( $sysop->getId(), $rev->getUser() );
1353 $this->assertSame( $sysop->getName(), $rev->getUserText() );
1354 }
1355
1356 /**
1357 * @covers Revision::isDeleted
1358 */
1359 public function testIsDeleted_nothingDeleted() {
1360 $rev = $this->testPage->getRevision();
1361
1362 $this->assertSame( false, $rev->isDeleted( Revision::DELETED_TEXT ) );
1363 $this->assertSame( false, $rev->isDeleted( Revision::DELETED_COMMENT ) );
1364 $this->assertSame( false, $rev->isDeleted( Revision::DELETED_RESTRICTED ) );
1365 $this->assertSame( false, $rev->isDeleted( Revision::DELETED_USER ) );
1366 }
1367
1368 /**
1369 * @covers Revision::getVisibility
1370 */
1371 public function testGetVisibility_nothingDeleted() {
1372 $rev = $this->testPage->getRevision();
1373
1374 $this->assertSame( 0, $rev->getVisibility() );
1375 }
1376
1377 /**
1378 * @covers Revision::getComment
1379 */
1380 public function testGetComment_notDeleted() {
1381 $expectedSummary = 'goatlicious summary';
1382
1383 $this->testPage->doEditContent(
1384 new WikitextContent( __METHOD__ ),
1385 $expectedSummary
1386 );
1387 $rev = $this->testPage->getRevision();
1388
1389 $this->assertSame( $expectedSummary, $rev->getComment() );
1390 }
1391
1392 /**
1393 * @covers Revision::isUnpatrolled
1394 */
1395 public function testIsUnpatrolled_returnsRecentChangesId() {
1396 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
1397 $rev = $this->testPage->getRevision();
1398
1399 $this->assertGreaterThan( 0, $rev->isUnpatrolled() );
1400 $this->assertSame( $rev->getRecentChange()->getAttribute( 'rc_id' ), $rev->isUnpatrolled() );
1401 }
1402
1403 /**
1404 * @covers Revision::isUnpatrolled
1405 */
1406 public function testIsUnpatrolled_returnsZeroIfPatrolled() {
1407 // This assumes that sysops are auto patrolled
1408 $sysop = $this->getTestSysop()->getUser();
1409 $this->testPage->doEditContent(
1410 new WikitextContent( __METHOD__ ),
1411 __METHOD__,
1412 0,
1413 false,
1414 $sysop
1415 );
1416 $rev = $this->testPage->getRevision();
1417
1418 $this->assertSame( 0, $rev->isUnpatrolled() );
1419 }
1420
1421 /**
1422 * This is a simple blanket test for all simple content getters and is methods to provide some
1423 * coverage before the split of Revision into multiple classes for MCR work.
1424 * @covers Revision::getContent
1425 * @covers Revision::getSerializedData
1426 * @covers Revision::getContentModel
1427 * @covers Revision::getContentFormat
1428 * @covers Revision::getContentHandler
1429 */
1430 public function testSimpleContentGetters() {
1431 $expectedText = 'testSimpleContentGetters in Revision. Goats love MCR...';
1432 $expectedSummary = 'goatlicious testSimpleContentGetters summary';
1433
1434 $this->testPage->doEditContent(
1435 new WikitextContent( $expectedText ),
1436 $expectedSummary
1437 );
1438 $rev = $this->testPage->getRevision();
1439
1440 $this->assertSame( $expectedText, $rev->getContent()->getText() );
1441 $this->assertSame( $expectedText, $rev->getSerializedData() );
1442 $this->assertSame( $this->testPage->getContentModel(), $rev->getContentModel() );
1443 $this->assertSame( $this->testPage->getContent()->getDefaultFormat(), $rev->getContentFormat() );
1444 $this->assertSame( $this->testPage->getContentHandler(), $rev->getContentHandler() );
1445 }
1446
1447 /**
1448 * @covers Revision::newKnownCurrent
1449 */
1450 public function testNewKnownCurrent() {
1451 // Setup the services
1452 $this->overrideMwServices();
1453 $cache = new WANObjectCache( [ 'cache' => new HashBagOStuff() ] );
1454 $this->setService( 'MainWANObjectCache', $cache );
1455 $db = wfGetDB( DB_MASTER );
1456
1457 $now = 1553893742;
1458 $cache->setMockTime( $now );
1459
1460 // Get a fresh revision to use during testing
1461 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
1462 $rev = $this->testPage->getRevision();
1463
1464 // Clear any previous cache for the revision during creation
1465 $key = $cache->makeGlobalKey(
1466 RevisionStore::ROW_CACHE_KEY,
1467 $db->getDomainID(),
1468 $rev->getPage(),
1469 $rev->getId()
1470 );
1471 $cache->delete( $key, WANObjectCache::HOLDOFF_TTL_NONE );
1472 $this->assertFalse( $cache->get( $key ) );
1473
1474 ++$now;
1475
1476 // Get the new revision and make sure it is in the cache and correct
1477 $newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
1478 $this->assertRevEquals( $rev, $newRev );
1479
1480 $cachedRow = $cache->get( $key );
1481 $this->assertNotFalse( $cachedRow );
1482 $this->assertEquals( $rev->getId(), $cachedRow->rev_id );
1483 }
1484
1485 public function testNewKnownCurrent_withPageId() {
1486 $db = wfGetDB( DB_MASTER );
1487
1488 $this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
1489 $rev = $this->testPage->getRevision();
1490
1491 $pageId = $this->testPage->getId();
1492
1493 $newRev = Revision::newKnownCurrent( $db, $pageId, $rev->getId() );
1494 $this->assertRevEquals( $rev, $newRev );
1495 }
1496
1497 public function testNewKnownCurrent_returnsFalseWhenTitleDoesntExist() {
1498 $db = wfGetDB( DB_MASTER );
1499
1500 $this->assertFalse( Revision::newKnownCurrent( $db, 0 ) );
1501 }
1502
1503 public function provideUserCanBitfield() {
1504 yield [ 0, 0, [], null, true ];
1505 // Bitfields match, user has no permissions
1506 yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], null, false ];
1507 yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], null, false ];
1508 yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], null, false ];
1509 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], null, false ];
1510 // Bitfields match, user (admin) does have permissions
1511 yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], null, true ];
1512 yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], null, true ];
1513 yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], null, true ];
1514 // Bitfields match, user (admin) does not have permissions
1515 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], null, false ];
1516 // Bitfields match, user (oversight) does have permissions
1517 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], null, true ];
1518 // Check permissions using the title
1519 yield [
1520 Revision::DELETED_TEXT,
1521 Revision::DELETED_TEXT,
1522 [ 'sysop' ],
1523 __METHOD__,
1524 true,
1525 ];
1526 yield [
1527 Revision::DELETED_TEXT,
1528 Revision::DELETED_TEXT,
1529 [],
1530 __METHOD__,
1531 false,
1532 ];
1533 }
1534
1535 /**
1536 * @dataProvider provideUserCanBitfield
1537 * @covers Revision::userCanBitfield
1538 */
1539 public function testUserCanBitfield( $bitField, $field, $userGroups, $title, $expected ) {
1540 $title = Title::newFromText( $title );
1541
1542 $this->setGroupPermissions(
1543 [
1544 'sysop' => [
1545 'deletedtext' => true,
1546 'deletedhistory' => true,
1547 ],
1548 'oversight' => [
1549 'viewsuppressed' => true,
1550 'suppressrevision' => true,
1551 ],
1552 ]
1553 );
1554 $user = $this->getTestUser( $userGroups )->getUser();
1555
1556 $this->assertSame(
1557 $expected,
1558 Revision::userCanBitfield( $bitField, $field, $user, $title )
1559 );
1560
1561 // Fallback to $wgUser
1562 $this->setMwGlobals(
1563 'wgUser',
1564 $user
1565 );
1566 $this->assertSame(
1567 $expected,
1568 Revision::userCanBitfield( $bitField, $field, null, $title )
1569 );
1570 }
1571
1572 public function provideUserCan() {
1573 yield [ 0, 0, [], true ];
1574 // Bitfields match, user has no permissions
1575 yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [], false ];
1576 yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [], false ];
1577 yield [ Revision::DELETED_USER, Revision::DELETED_USER, [], false ];
1578 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [], false ];
1579 // Bitfields match, user (admin) does have permissions
1580 yield [ Revision::DELETED_TEXT, Revision::DELETED_TEXT, [ 'sysop' ], true ];
1581 yield [ Revision::DELETED_COMMENT, Revision::DELETED_COMMENT, [ 'sysop' ], true ];
1582 yield [ Revision::DELETED_USER, Revision::DELETED_USER, [ 'sysop' ], true ];
1583 // Bitfields match, user (admin) does not have permissions
1584 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'sysop' ], false ];
1585 // Bitfields match, user (oversight) does have permissions
1586 yield [ Revision::DELETED_RESTRICTED, Revision::DELETED_RESTRICTED, [ 'oversight' ], true ];
1587 }
1588
1589 /**
1590 * @dataProvider provideUserCan
1591 * @covers Revision::userCan
1592 */
1593 public function testUserCan( $bitField, $field, $userGroups, $expected ) {
1594 $this->setGroupPermissions(
1595 [
1596 'sysop' => [
1597 'deletedtext' => true,
1598 'deletedhistory' => true,
1599 ],
1600 'oversight' => [
1601 'viewsuppressed' => true,
1602 'suppressrevision' => true,
1603 ],
1604 ]
1605 );
1606 $user = $this->getTestUser( $userGroups )->getUser();
1607 $revision = new Revision( [ 'deleted' => $bitField ], 0, $this->testPage->getTitle() );
1608
1609 $this->assertSame(
1610 $expected,
1611 $revision->userCan( $field, $user )
1612 );
1613 }
1614
1615 public function provideGetTextId() {
1616 yield [ [], null ];
1617
1618 $slot = new SlotRecord( (object)[
1619 'slot_revision_id' => 42,
1620 'slot_content_id' => 1,
1621 'content_address' => 'tt:789',
1622 'model_name' => CONTENT_MODEL_WIKITEXT,
1623 'role_name' => SlotRecord::MAIN,
1624 'slot_origin' => 1,
1625 ], new WikitextContent( 'Test' ) );
1626
1627 $rec = new MutableRevisionRecord( $this->testPage->getTitle() );
1628 $rec->setId( 42 );
1629 $rec->setSlot( $slot );
1630
1631 yield [ $rec, 789 ];
1632 }
1633
1634 /**
1635 * @dataProvider provideGetTextId
1636 * @covers Revision::getTextId()
1637 */
1638 public function testGetTextId( $spec, $expected ) {
1639 $rev = new Revision( $spec, 0, $this->testPage->getTitle() );
1640 $this->assertSame( $expected, $rev->getTextId() );
1641 }
1642
1643 abstract public function provideGetRevisionText();
1644
1645 /**
1646 * @dataProvider provideGetRevisionText
1647 * @covers Revision::getRevisionText
1648 */
1649 public function testGetRevisionText( array $queryInfoOptions, array $queryInfoExtra = [] ) {
1650 $rev = $this->testPage->getRevisionRecord();
1651
1652 $queryInfo = Revision::getQueryInfo( $queryInfoOptions );
1653 $queryInfo['tables'] = array_merge( $queryInfo['tables'], $queryInfoExtra['tables'] ?? [] );
1654 $queryInfo['fields'] = array_merge( $queryInfo['fields'], $queryInfoExtra['fields'] ?? [] );
1655 $queryInfo['joins'] = array_merge( $queryInfo['joins'], $queryInfoExtra['joins'] ?? [] );
1656
1657 $conds = [ 'rev_id' => $rev->getId() ];
1658 $row = $this->db->selectRow(
1659 $queryInfo['tables'],
1660 $queryInfo['fields'],
1661 $conds,
1662 __METHOD__,
1663 [],
1664 $queryInfo['joins']
1665 );
1666
1667 $expected = $rev->getContent( SlotRecord::MAIN )->serialize();
1668
1669 $this->hideDeprecated( 'Revision::getRevisionText (MCR without SCHEMA_COMPAT_WRITE_OLD)' );
1670 $this->assertSame( $expected, Revision::getRevisionText( $row ) );
1671 }
1672
1673 }