3 namespace MediaWiki\Tests\Storage
;
5 use CommentStoreComment
;
7 use MediaWiki\MediaWikiServices
;
8 use MediaWiki\Storage\RevisionRecord
;
17 * @covers \MediaWiki\Storage\PageUpdater
20 class PageUpdaterTest
extends MediaWikiTestCase
{
22 private function getDummyTitle( $method ) {
23 return Title
::newFromText( $method, $this->getDefaultWikitextNS() );
29 * @return null|RecentChange
31 private function getRecentChangeFor( $revId ) {
32 $qi = RecentChange
::getQueryInfo();
33 $row = $this->db
->selectRow(
36 [ 'rc_this_oldid' => $revId ],
42 return $row ? RecentChange
::newFromRow( $row ) : null;
45 // TODO: test setAjaxEditStash();
48 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
49 * @covers \WikiPage::newPageUpdater()
51 public function testCreatePage() {
52 $user = $this->getTestUser()->getUser();
54 $title = $this->getDummyTitle( __METHOD__
);
55 $page = WikiPage
::factory( $title );
56 $updater = $page->newPageUpdater( $user );
58 $oldStats = $this->db
->selectRow( 'site_stats', '*', '1=1' );
60 $this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
61 $this->assertFalse( $updater->getOriginalRevisionId(), 'getOriginalRevisionId' );
62 $this->assertSame( 0, $updater->getUndidRevisionId(), 'getUndidRevisionId' );
64 $updater->addTag( 'foo' );
65 $updater->addTags( [ 'bar', 'qux' ] );
67 $tags = $updater->getExplicitTags();
69 $this->assertSame( [ 'bar', 'foo', 'qux' ], $tags, 'getExplicitTags' );
71 // TODO: MCR: test additional slots
72 $content = new TextContent( 'Lorem Ipsum' );
73 $updater->setContent( 'main', $content );
75 $parent = $updater->grabParentRevision();
77 $this->assertNull( $parent, 'getParentRevision' );
78 $this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
80 // TODO: test that hasEditConflict() grabs the parent revision
81 $this->assertFalse( $updater->hasEditConflict( 0 ), 'hasEditConflict' );
82 $this->assertTrue( $updater->hasEditConflict( 1 ), 'hasEditConflict' );
84 // TODO: test failure with EDIT_UPDATE
85 // TODO: test EDIT_MINOR, EDIT_BOT, etc
86 $summary = CommentStoreComment
::newUnsavedComment( 'Just a test' );
87 $rev = $updater->saveRevision( $summary );
89 $this->assertNotNull( $rev );
90 $this->assertSame( 0, $rev->getParentId() );
91 $this->assertSame( $summary->text
, $rev->getComment( RevisionRecord
::RAW
)->text
);
92 $this->assertSame( $user->getName(), $rev->getUser( RevisionRecord
::RAW
)->getName() );
94 $this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
95 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
96 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
97 $this->assertTrue( $updater->isNew(), 'isNew()' );
98 $this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
99 $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
100 $this->assertInstanceOf( Revision
::class, $updater->getStatus()->value
['revision'] );
102 $rev = $updater->getNewRevision();
103 $revContent = $rev->getContent( 'main' );
104 $this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
106 // were the WikiPage and Title objects updated?
107 $this->assertTrue( $page->exists(), 'WikiPage::exists()' );
108 $this->assertTrue( $title->exists(), 'Title::exists()' );
109 $this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
110 $this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
113 $page2 = WikiPage
::factory( $title );
114 $this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
115 $this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
116 $this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
119 $rc = $this->getRecentChangeFor( $rev->getId() );
120 $this->assertNotNull( $rc, 'RecentChange' );
122 // check site stats - this asserts that derived data updates where run.
123 $stats = $this->db
->selectRow( 'site_stats', '*', '1=1' );
124 $this->assertSame( $oldStats->ss_total_pages +
1, (int)$stats->ss_total_pages
);
125 $this->assertSame( $oldStats->ss_total_edits +
1, (int)$stats->ss_total_edits
);
127 // re-edit with same content - should be a "null-edit"
128 $updater = $page->newPageUpdater( $user );
129 $updater->setContent( 'main', $content );
131 $summary = CommentStoreComment
::newUnsavedComment( 'to to re-edit' );
132 $rev = $updater->saveRevision( $summary );
133 $status = $updater->getStatus();
135 $this->assertNull( $rev, 'getNewRevision()' );
136 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
137 $this->assertTrue( $updater->isUnchanged(), 'isUnchanged' );
138 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
139 $this->assertTrue( $status->isOK(), 'getStatus()->isOK()' );
140 $this->assertTrue( $status->hasMessage( 'edit-no-change' ), 'edit-no-change' );
144 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
145 * @covers \WikiPage::newPageUpdater()
147 public function testUpdatePage() {
148 $user = $this->getTestUser()->getUser();
150 $title = $this->getDummyTitle( __METHOD__
);
151 $this->insertPage( $title );
153 $page = WikiPage
::factory( $title );
154 $parentId = $page->getLatest();
156 $updater = $page->newPageUpdater( $user );
158 $oldStats = $this->db
->selectRow( 'site_stats', '*', '1=1' );
160 $updater->setOriginalRevisionId( 7 );
161 $this->assertSame( 7, $updater->getOriginalRevisionId(), 'getOriginalRevisionId' );
163 $this->assertFalse( $updater->hasEditConflict( $parentId ), 'hasEditConflict' );
164 $this->assertTrue( $updater->hasEditConflict( $parentId - 1 ), 'hasEditConflict' );
165 $this->assertTrue( $updater->hasEditConflict( 0 ), 'hasEditConflict' );
167 // TODO: MCR: test additional slots
168 $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
170 // TODO: test all flags for saveRevision()!
171 $summary = CommentStoreComment
::newUnsavedComment( 'Just a test' );
172 $rev = $updater->saveRevision( $summary );
174 $this->assertNotNull( $rev );
175 $this->assertSame( $parentId, $rev->getParentId() );
176 $this->assertSame( $summary->text
, $rev->getComment( RevisionRecord
::RAW
)->text
);
177 $this->assertSame( $user->getName(), $rev->getUser( RevisionRecord
::RAW
)->getName() );
179 $this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
180 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
181 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
182 $this->assertFalse( $updater->isNew(), 'isNew()' );
183 $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
184 $this->assertInstanceOf( Revision
::class, $updater->getStatus()->value
['revision'] );
185 $this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
187 // TODO: Test null revision (with different user): new revision!
189 $rev = $updater->getNewRevision();
190 $revContent = $rev->getContent( 'main' );
191 $this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
193 // were the WikiPage and Title objects updated?
194 $this->assertTrue( $page->exists(), 'WikiPage::exists()' );
195 $this->assertTrue( $title->exists(), 'Title::exists()' );
196 $this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
197 $this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
200 $page2 = WikiPage
::factory( $title );
201 $this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
202 $this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
203 $this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
206 $rc = $this->getRecentChangeFor( $rev->getId() );
207 $this->assertNotNull( $rc, 'RecentChange' );
210 $updater = $page->newPageUpdater( $user );
211 $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
213 $summary = CommentStoreComment
::newUnsavedComment( 're-edit' );
214 $updater->saveRevision( $summary );
215 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
216 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
218 // check site stats - this asserts that derived data updates where run.
219 $stats = $this->db
->selectRow( 'site_stats', '*', '1=1' );
220 $this->assertSame( $oldStats->ss_total_pages +
0, (int)$stats->ss_total_pages
);
221 $this->assertSame( $oldStats->ss_total_edits +
2, (int)$stats->ss_total_edits
);
225 * Creates a revision in the database.
227 * @param WikiPage $page
229 * @param null|string|Content $content
231 * @return RevisionRecord|null
233 private function createRevision( WikiPage
$page, $summary, $content = null ) {
234 $user = $this->getTestUser()->getUser();
235 $comment = CommentStoreComment
::newUnsavedComment( $summary );
237 if ( !$content instanceof Content
) {
238 $content = new TextContent( $content ??
$summary );
241 $updater = $page->newPageUpdater( $user );
242 $updater->setContent( 'main', $content );
243 $rev = $updater->saveRevision( $comment );
248 * @covers \MediaWiki\Storage\PageUpdater::grabParentRevision()
249 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
251 public function testCompareAndSwapFailure() {
252 $user = $this->getTestUser()->getUser();
254 $title = $this->getDummyTitle( __METHOD__
);
256 // start editing non-existing page
257 $page = WikiPage
::factory( $title );
258 $updater = $page->newPageUpdater( $user );
259 $updater->grabParentRevision();
261 // create page concurrently
262 $concurrentPage = WikiPage
::factory( $title );
263 $this->createRevision( $concurrentPage, __METHOD__
. '-one' );
265 // try creating the page - should trigger CAS failure.
266 $summary = CommentStoreComment
::newUnsavedComment( 'create?!' );
267 $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
268 $updater->saveRevision( $summary );
269 $status = $updater->getStatus();
271 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
272 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
273 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
274 $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-conflict' );
276 // start editing existing page
277 $page = WikiPage
::factory( $title );
278 $updater = $page->newPageUpdater( $user );
279 $updater->grabParentRevision();
281 // update page concurrently
282 $concurrentPage = WikiPage
::factory( $title );
283 $this->createRevision( $concurrentPage, __METHOD__
. '-two' );
285 // try creating the page - should trigger CAS failure.
286 $summary = CommentStoreComment
::newUnsavedComment( 'edit?!' );
287 $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
288 $updater->saveRevision( $summary );
289 $status = $updater->getStatus();
291 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
292 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
293 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
294 $this->assertTrue( $status->hasMessage( 'edit-conflict' ), 'edit-conflict' );
298 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
300 public function testFailureOnEditFlags() {
301 $user = $this->getTestUser()->getUser();
303 $title = $this->getDummyTitle( __METHOD__
);
305 // start editing non-existing page
306 $page = WikiPage
::factory( $title );
307 $updater = $page->newPageUpdater( $user );
309 // update with EDIT_UPDATE flag should fail
310 $summary = CommentStoreComment
::newUnsavedComment( 'udpate?!' );
311 $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
312 $updater->saveRevision( $summary, EDIT_UPDATE
);
313 $status = $updater->getStatus();
315 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
316 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
317 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
318 $this->assertTrue( $status->hasMessage( 'edit-gone-missing' ), 'edit-gone-missing' );
321 $this->createRevision( $page, __METHOD__
);
323 // update with EDIT_NEW flag should fail
324 $summary = CommentStoreComment
::newUnsavedComment( 'create?!' );
325 $updater = $page->newPageUpdater( $user );
326 $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
327 $updater->saveRevision( $summary, EDIT_NEW
);
328 $status = $updater->getStatus();
330 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
331 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
332 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
333 $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-already-exists' );
336 public function provideSetRcPatrolStatus( $patrolled ) {
337 yield
[ RecentChange
::PRC_UNPATROLLED
];
338 yield
[ RecentChange
::PRC_AUTOPATROLLED
];
342 * @dataProvider provideSetRcPatrolStatus
343 * @covers \MediaWiki\Storage\PageUpdater::setRcPatrolStatus()
345 public function testSetRcPatrolStatus( $patrolled ) {
346 $revisionStore = MediaWikiServices
::getInstance()->getRevisionStore();
348 $user = $this->getTestUser()->getUser();
350 $title = $this->getDummyTitle( __METHOD__
);
352 $page = WikiPage
::factory( $title );
353 $updater = $page->newPageUpdater( $user );
355 $summary = CommentStoreComment
::newUnsavedComment( 'Lorem ipsum ' . $patrolled );
356 $updater->setContent( 'main', new TextContent( 'Lorem ipsum ' . $patrolled ) );
357 $updater->setRcPatrolStatus( $patrolled );
358 $rev = $updater->saveRevision( $summary );
360 $rc = $revisionStore->getRecentChange( $rev );
361 $this->assertEquals( $patrolled, $rc->getAttribute( 'rc_patrolled' ) );
365 * @covers \MediaWiki\Storage\PageUpdater::inheritSlot()
366 * @covers \MediaWiki\Storage\PageUpdater::setContent()
368 public function testInheritSlot() {
369 $user = $this->getTestUser()->getUser();
370 $title = $this->getDummyTitle( __METHOD__
);
371 $page = WikiPage
::factory( $title );
373 $updater = $page->newPageUpdater( $user );
374 $summary = CommentStoreComment
::newUnsavedComment( 'one' );
375 $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
376 $rev1 = $updater->saveRevision( $summary, EDIT_NEW
);
378 $updater = $page->newPageUpdater( $user );
379 $summary = CommentStoreComment
::newUnsavedComment( 'two' );
380 $updater->setContent( 'main', new TextContent( 'Foo Bar' ) );
381 $rev2 = $updater->saveRevision( $summary, EDIT_UPDATE
);
383 $updater = $page->newPageUpdater( $user );
384 $summary = CommentStoreComment
::newUnsavedComment( 'three' );
385 $updater->inheritSlot( $rev1->getSlot( 'main' ) );
386 $rev3 = $updater->saveRevision( $summary, EDIT_UPDATE
);
388 $this->assertNotSame( $rev1->getId(), $rev3->getId() );
389 $this->assertNotSame( $rev2->getId(), $rev3->getId() );
391 $main1 = $rev1->getSlot( 'main' );
392 $main3 = $rev3->getSlot( 'main' );
394 $this->assertNotSame( $main1->getRevision(), $main3->getRevision() );
395 $this->assertSame( $main1->getAddress(), $main3->getAddress() );
396 $this->assertTrue( $main1->getContent()->equals( $main3->getContent() ) );
399 // TODO: MCR: test adding multiple slots, inheriting parent slots, and removing slots.
401 public function testSetUseAutomaticEditSummaries() {
402 $this->setContentLang( 'qqx' );
403 $user = $this->getTestUser()->getUser();
405 $title = $this->getDummyTitle( __METHOD__
);
406 $page = WikiPage
::factory( $title );
408 $updater = $page->newPageUpdater( $user );
409 $updater->setUseAutomaticEditSummaries( true );
410 $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
412 // empty comment triggers auto-summary
413 $summary = CommentStoreComment
::newUnsavedComment( '' );
414 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY
);
416 $rev = $updater->getNewRevision();
417 $comment = $rev->getComment( RevisionRecord
::RAW
);
418 $this->assertSame( '(autosumm-new: Lorem Ipsum)', $comment->text
, 'comment text' );
420 // check that this also works when blanking the page
421 $updater = $page->newPageUpdater( $user );
422 $updater->setUseAutomaticEditSummaries( true );
423 $updater->setContent( 'main', new TextContent( '' ) );
425 $summary = CommentStoreComment
::newUnsavedComment( '' );
426 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY
);
428 $rev = $updater->getNewRevision();
429 $comment = $rev->getComment( RevisionRecord
::RAW
);
430 $this->assertSame( '(autosumm-blank)', $comment->text
, 'comment text' );
432 // check that we can also disable edit-summaries
433 $title2 = $this->getDummyTitle( __METHOD__
. '/2' );
434 $page2 = WikiPage
::factory( $title2 );
436 $updater = $page2->newPageUpdater( $user );
437 $updater->setUseAutomaticEditSummaries( false );
438 $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
440 $summary = CommentStoreComment
::newUnsavedComment( '' );
441 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY
);
443 $rev = $updater->getNewRevision();
444 $comment = $rev->getComment( RevisionRecord
::RAW
);
445 $this->assertSame( '', $comment->text
, 'comment text should still be lank' );
447 // check that we don't do auto.summaries without the EDIT_AUTOSUMMARY flag
448 $updater = $page2->newPageUpdater( $user );
449 $updater->setUseAutomaticEditSummaries( true );
450 $updater->setContent( 'main', new TextContent( '' ) );
452 $summary = CommentStoreComment
::newUnsavedComment( '' );
453 $updater->saveRevision( $summary, 0 );
455 $rev = $updater->getNewRevision();
456 $comment = $rev->getComment( RevisionRecord
::RAW
);
457 $this->assertSame( '', $comment->text
, 'comment text' );
460 public function provideSetUsePageCreationLog() {
461 yield
[ true, [ [ 'create', 'create' ] ] ];
466 * @dataProvider provideSetUsePageCreationLog
469 public function testSetUsePageCreationLog( $use, $expected ) {
470 $user = $this->getTestUser()->getUser();
471 $title = $this->getDummyTitle( __METHOD__
. ( $use ?
'_logged' : '_unlogged' ) );
472 $page = WikiPage
::factory( $title );
474 $updater = $page->newPageUpdater( $user );
475 $updater->setUsePageCreationLog( $use );
476 $summary = CommentStoreComment
::newUnsavedComment( 'cmt' );
477 $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
478 $updater->saveRevision( $summary, EDIT_NEW
);
480 $rev = $updater->getNewRevision();
483 [ 'log_type', 'log_action' ],
484 [ 'log_page' => $rev->getPageId() ],