Merge "MCR: Add temporary web UI mcrundo action"
[lhc/web/wiklou.git] / tests / phpunit / includes / Storage / PageUpdaterTest.php
1 <?php
2
3 namespace MediaWiki\Tests\Storage;
4
5 use CommentStoreComment;
6 use Content;
7 use MediaWiki\MediaWikiServices;
8 use MediaWiki\Storage\RevisionRecord;
9 use MediaWikiTestCase;
10 use RecentChange;
11 use Revision;
12 use TextContent;
13 use Title;
14 use WikiPage;
15
16 /**
17 * @covers \MediaWiki\Storage\PageUpdater
18 * @group Database
19 */
20 class PageUpdaterTest extends MediaWikiTestCase {
21
22 public static function setUpBeforeClass() {
23 parent::setUpBeforeClass();
24
25 // force service reset!
26 MediaWikiServices::getInstance()->resetServiceForTesting( 'RevisionStore' );
27 }
28
29 private function getDummyTitle( $method ) {
30 return Title::newFromText( $method, $this->getDefaultWikitextNS() );
31 }
32
33 /**
34 * @param int $revId
35 *
36 * @return null|RecentChange
37 */
38 private function getRecentChangeFor( $revId ) {
39 $qi = RecentChange::getQueryInfo();
40 $row = $this->db->selectRow(
41 $qi['tables'],
42 $qi['fields'],
43 [ 'rc_this_oldid' => $revId ],
44 __METHOD__,
45 [],
46 $qi['joins']
47 );
48
49 return $row ? RecentChange::newFromRow( $row ) : null;
50 }
51
52 // TODO: test setAjaxEditStash();
53
54 /**
55 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
56 * @covers \WikiPage::newPageUpdater()
57 */
58 public function testCreatePage() {
59 $user = $this->getTestUser()->getUser();
60
61 $title = $this->getDummyTitle( __METHOD__ );
62 $page = WikiPage::factory( $title );
63 $updater = $page->newPageUpdater( $user );
64
65 $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
66
67 $this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
68 $this->assertFalse( $updater->getOriginalRevisionId(), 'getOriginalRevisionId' );
69 $this->assertSame( 0, $updater->getUndidRevisionId(), 'getUndidRevisionId' );
70
71 $updater->addTag( 'foo' );
72 $updater->addTags( [ 'bar', 'qux' ] );
73
74 $tags = $updater->getExplicitTags();
75 sort( $tags );
76 $this->assertSame( [ 'bar', 'foo', 'qux' ], $tags, 'getExplicitTags' );
77
78 // TODO: MCR: test additional slots
79 $content = new TextContent( 'Lorem Ipsum' );
80 $updater->setContent( 'main', $content );
81
82 $parent = $updater->grabParentRevision();
83
84 $this->assertNull( $parent, 'getParentRevision' );
85 $this->assertFalse( $updater->wasCommitted(), 'wasCommitted' );
86
87 // TODO: test that hasEditConflict() grabs the parent revision
88 $this->assertFalse( $updater->hasEditConflict( 0 ), 'hasEditConflict' );
89 $this->assertTrue( $updater->hasEditConflict( 1 ), 'hasEditConflict' );
90
91 // TODO: test failure with EDIT_UPDATE
92 // TODO: test EDIT_MINOR, EDIT_BOT, etc
93 $summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
94 $rev = $updater->saveRevision( $summary );
95
96 $this->assertNotNull( $rev );
97 $this->assertSame( 0, $rev->getParentId() );
98 $this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text );
99 $this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() );
100
101 $this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
102 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
103 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
104 $this->assertTrue( $updater->isNew(), 'isNew()' );
105 $this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
106 $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
107 $this->assertInstanceOf( Revision::class, $updater->getStatus()->value['revision'] );
108
109 $rev = $updater->getNewRevision();
110 $revContent = $rev->getContent( 'main' );
111 $this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
112
113 // were the WikiPage and Title objects updated?
114 $this->assertTrue( $page->exists(), 'WikiPage::exists()' );
115 $this->assertTrue( $title->exists(), 'Title::exists()' );
116 $this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
117 $this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
118
119 // re-load
120 $page2 = WikiPage::factory( $title );
121 $this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
122 $this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
123 $this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
124
125 // Check RC entry
126 $rc = $this->getRecentChangeFor( $rev->getId() );
127 $this->assertNotNull( $rc, 'RecentChange' );
128
129 // check site stats - this asserts that derived data updates where run.
130 $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
131 $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
132 $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
133
134 // re-edit with same content - should be a "null-edit"
135 $updater = $page->newPageUpdater( $user );
136 $updater->setContent( 'main', $content );
137
138 $summary = CommentStoreComment::newUnsavedComment( 'to to re-edit' );
139 $rev = $updater->saveRevision( $summary );
140 $status = $updater->getStatus();
141
142 $this->assertNull( $rev, 'getNewRevision()' );
143 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
144 $this->assertTrue( $updater->isUnchanged(), 'isUnchanged' );
145 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
146 $this->assertTrue( $status->isOK(), 'getStatus()->isOK()' );
147 $this->assertTrue( $status->hasMessage( 'edit-no-change' ), 'edit-no-change' );
148 }
149
150 /**
151 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
152 * @covers \WikiPage::newPageUpdater()
153 */
154 public function testUpdatePage() {
155 $user = $this->getTestUser()->getUser();
156
157 $title = $this->getDummyTitle( __METHOD__ );
158 $this->insertPage( $title );
159
160 $page = WikiPage::factory( $title );
161 $parentId = $page->getLatest();
162
163 $updater = $page->newPageUpdater( $user );
164
165 $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
166
167 $updater->setOriginalRevisionId( 7 );
168 $this->assertSame( 7, $updater->getOriginalRevisionId(), 'getOriginalRevisionId' );
169
170 $this->assertFalse( $updater->hasEditConflict( $parentId ), 'hasEditConflict' );
171 $this->assertTrue( $updater->hasEditConflict( $parentId - 1 ), 'hasEditConflict' );
172 $this->assertTrue( $updater->hasEditConflict( 0 ), 'hasEditConflict' );
173
174 // TODO: MCR: test additional slots
175 $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
176
177 // TODO: test all flags for saveRevision()!
178 $summary = CommentStoreComment::newUnsavedComment( 'Just a test' );
179 $rev = $updater->saveRevision( $summary );
180
181 $this->assertNotNull( $rev );
182 $this->assertSame( $parentId, $rev->getParentId() );
183 $this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text );
184 $this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() );
185
186 $this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' );
187 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
188 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
189 $this->assertFalse( $updater->isNew(), 'isNew()' );
190 $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' );
191 $this->assertInstanceOf( Revision::class, $updater->getStatus()->value['revision'] );
192 $this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' );
193
194 // TODO: Test null revision (with different user): new revision!
195
196 $rev = $updater->getNewRevision();
197 $revContent = $rev->getContent( 'main' );
198 $this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' );
199
200 // were the WikiPage and Title objects updated?
201 $this->assertTrue( $page->exists(), 'WikiPage::exists()' );
202 $this->assertTrue( $title->exists(), 'Title::exists()' );
203 $this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getRevision()' );
204 $this->assertNotNull( $page->getRevision(), 'WikiPage::getRevision()' );
205
206 // re-load
207 $page2 = WikiPage::factory( $title );
208 $this->assertTrue( $page2->exists(), 'WikiPage::exists()' );
209 $this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getRevision()' );
210 $this->assertNotNull( $page2->getRevision(), 'WikiPage::getRevision()' );
211
212 // Check RC entry
213 $rc = $this->getRecentChangeFor( $rev->getId() );
214 $this->assertNotNull( $rc, 'RecentChange' );
215
216 // re-edit
217 $updater = $page->newPageUpdater( $user );
218 $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
219
220 $summary = CommentStoreComment::newUnsavedComment( 're-edit' );
221 $updater->saveRevision( $summary );
222 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' );
223 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' );
224
225 // check site stats - this asserts that derived data updates where run.
226 $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
227 $this->assertNotNull( $stats, 'site_stats' );
228 $this->assertSame( $oldStats->ss_total_pages + 0, (int)$stats->ss_total_pages );
229 $this->assertSame( $oldStats->ss_total_edits + 2, (int)$stats->ss_total_edits );
230 }
231
232 /**
233 * Creates a revision in the database.
234 *
235 * @param WikiPage $page
236 * @param $summary
237 * @param null|string|Content $content
238 *
239 * @return RevisionRecord|null
240 */
241 private function createRevision( WikiPage $page, $summary, $content = null ) {
242 $user = $this->getTestUser()->getUser();
243 $comment = CommentStoreComment::newUnsavedComment( $summary );
244
245 if ( !$content instanceof Content ) {
246 $content = new TextContent( $content ?? $summary );
247 }
248
249 $updater = $page->newPageUpdater( $user );
250 $updater->setContent( 'main', $content );
251 $rev = $updater->saveRevision( $comment );
252 return $rev;
253 }
254
255 /**
256 * @covers \MediaWiki\Storage\PageUpdater::grabParentRevision()
257 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
258 */
259 public function testCompareAndSwapFailure() {
260 $user = $this->getTestUser()->getUser();
261
262 $title = $this->getDummyTitle( __METHOD__ );
263
264 // start editing non-existing page
265 $page = WikiPage::factory( $title );
266 $updater = $page->newPageUpdater( $user );
267 $updater->grabParentRevision();
268
269 // create page concurrently
270 $concurrentPage = WikiPage::factory( $title );
271 $this->createRevision( $concurrentPage, __METHOD__ . '-one' );
272
273 // try creating the page - should trigger CAS failure.
274 $summary = CommentStoreComment::newUnsavedComment( 'create?!' );
275 $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
276 $updater->saveRevision( $summary );
277 $status = $updater->getStatus();
278
279 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
280 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
281 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
282 $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-conflict' );
283
284 // start editing existing page
285 $page = WikiPage::factory( $title );
286 $updater = $page->newPageUpdater( $user );
287 $updater->grabParentRevision();
288
289 // update page concurrently
290 $concurrentPage = WikiPage::factory( $title );
291 $this->createRevision( $concurrentPage, __METHOD__ . '-two' );
292
293 // try creating the page - should trigger CAS failure.
294 $summary = CommentStoreComment::newUnsavedComment( 'edit?!' );
295 $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
296 $updater->saveRevision( $summary );
297 $status = $updater->getStatus();
298
299 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
300 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
301 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
302 $this->assertTrue( $status->hasMessage( 'edit-conflict' ), 'edit-conflict' );
303 }
304
305 /**
306 * @covers \MediaWiki\Storage\PageUpdater::saveRevision()
307 */
308 public function testFailureOnEditFlags() {
309 $user = $this->getTestUser()->getUser();
310
311 $title = $this->getDummyTitle( __METHOD__ );
312
313 // start editing non-existing page
314 $page = WikiPage::factory( $title );
315 $updater = $page->newPageUpdater( $user );
316
317 // update with EDIT_UPDATE flag should fail
318 $summary = CommentStoreComment::newUnsavedComment( 'udpate?!' );
319 $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
320 $updater->saveRevision( $summary, EDIT_UPDATE );
321 $status = $updater->getStatus();
322
323 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
324 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
325 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
326 $this->assertTrue( $status->hasMessage( 'edit-gone-missing' ), 'edit-gone-missing' );
327
328 // create the page
329 $this->createRevision( $page, __METHOD__ );
330
331 // update with EDIT_NEW flag should fail
332 $summary = CommentStoreComment::newUnsavedComment( 'create?!' );
333 $updater = $page->newPageUpdater( $user );
334 $updater->setContent( 'main', new TextContent( 'dolor sit amet' ) );
335 $updater->saveRevision( $summary, EDIT_NEW );
336 $status = $updater->getStatus();
337
338 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' );
339 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' );
340 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' );
341 $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-already-exists' );
342 }
343
344 public function provideSetRcPatrolStatus( $patrolled ) {
345 yield [ RecentChange::PRC_UNPATROLLED ];
346 yield [ RecentChange::PRC_AUTOPATROLLED ];
347 }
348
349 /**
350 * @dataProvider provideSetRcPatrolStatus
351 * @covers \MediaWiki\Storage\PageUpdater::setRcPatrolStatus()
352 */
353 public function testSetRcPatrolStatus( $patrolled ) {
354 $revisionStore = MediaWikiServices::getInstance()->getRevisionStore();
355
356 $user = $this->getTestUser()->getUser();
357
358 $title = $this->getDummyTitle( __METHOD__ );
359
360 $page = WikiPage::factory( $title );
361 $updater = $page->newPageUpdater( $user );
362
363 $summary = CommentStoreComment::newUnsavedComment( 'Lorem ipsum ' . $patrolled );
364 $updater->setContent( 'main', new TextContent( 'Lorem ipsum ' . $patrolled ) );
365 $updater->setRcPatrolStatus( $patrolled );
366 $rev = $updater->saveRevision( $summary );
367
368 $rc = $revisionStore->getRecentChange( $rev );
369 $this->assertEquals( $patrolled, $rc->getAttribute( 'rc_patrolled' ) );
370 }
371
372 /**
373 * @covers \MediaWiki\Storage\PageUpdater::inheritSlot()
374 * @covers \MediaWiki\Storage\PageUpdater::setContent()
375 */
376 public function testInheritSlot() {
377 $user = $this->getTestUser()->getUser();
378 $title = $this->getDummyTitle( __METHOD__ );
379 $page = WikiPage::factory( $title );
380
381 $updater = $page->newPageUpdater( $user );
382 $summary = CommentStoreComment::newUnsavedComment( 'one' );
383 $updater->setContent( 'main', new TextContent( 'Lorem ipsum' ) );
384 $rev1 = $updater->saveRevision( $summary, EDIT_NEW );
385
386 $updater = $page->newPageUpdater( $user );
387 $summary = CommentStoreComment::newUnsavedComment( 'two' );
388 $updater->setContent( 'main', new TextContent( 'Foo Bar' ) );
389 $rev2 = $updater->saveRevision( $summary, EDIT_UPDATE );
390
391 $updater = $page->newPageUpdater( $user );
392 $summary = CommentStoreComment::newUnsavedComment( 'three' );
393 $updater->inheritSlot( $rev1->getSlot( 'main' ) );
394 $rev3 = $updater->saveRevision( $summary, EDIT_UPDATE );
395
396 $this->assertNotSame( $rev1->getId(), $rev3->getId() );
397 $this->assertNotSame( $rev2->getId(), $rev3->getId() );
398
399 $main1 = $rev1->getSlot( 'main' );
400 $main3 = $rev3->getSlot( 'main' );
401
402 $this->assertNotSame( $main1->getRevision(), $main3->getRevision() );
403 $this->assertSame( $main1->getAddress(), $main3->getAddress() );
404 $this->assertTrue( $main1->getContent()->equals( $main3->getContent() ) );
405 }
406
407 // TODO: MCR: test adding multiple slots, inheriting parent slots, and removing slots.
408
409 public function testSetUseAutomaticEditSummaries() {
410 $this->setContentLang( 'qqx' );
411 $user = $this->getTestUser()->getUser();
412
413 $title = $this->getDummyTitle( __METHOD__ );
414 $page = WikiPage::factory( $title );
415
416 $updater = $page->newPageUpdater( $user );
417 $updater->setUseAutomaticEditSummaries( true );
418 $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
419
420 // empty comment triggers auto-summary
421 $summary = CommentStoreComment::newUnsavedComment( '' );
422 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
423
424 $rev = $updater->getNewRevision();
425 $comment = $rev->getComment( RevisionRecord::RAW );
426 $this->assertSame( '(autosumm-new: Lorem Ipsum)', $comment->text, 'comment text' );
427
428 // check that this also works when blanking the page
429 $updater = $page->newPageUpdater( $user );
430 $updater->setUseAutomaticEditSummaries( true );
431 $updater->setContent( 'main', new TextContent( '' ) );
432
433 $summary = CommentStoreComment::newUnsavedComment( '' );
434 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
435
436 $rev = $updater->getNewRevision();
437 $comment = $rev->getComment( RevisionRecord::RAW );
438 $this->assertSame( '(autosumm-blank)', $comment->text, 'comment text' );
439
440 // check that we can also disable edit-summaries
441 $title2 = $this->getDummyTitle( __METHOD__ . '/2' );
442 $page2 = WikiPage::factory( $title2 );
443
444 $updater = $page2->newPageUpdater( $user );
445 $updater->setUseAutomaticEditSummaries( false );
446 $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
447
448 $summary = CommentStoreComment::newUnsavedComment( '' );
449 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY );
450
451 $rev = $updater->getNewRevision();
452 $comment = $rev->getComment( RevisionRecord::RAW );
453 $this->assertSame( '', $comment->text, 'comment text should still be lank' );
454
455 // check that we don't do auto.summaries without the EDIT_AUTOSUMMARY flag
456 $updater = $page2->newPageUpdater( $user );
457 $updater->setUseAutomaticEditSummaries( true );
458 $updater->setContent( 'main', new TextContent( '' ) );
459
460 $summary = CommentStoreComment::newUnsavedComment( '' );
461 $updater->saveRevision( $summary, 0 );
462
463 $rev = $updater->getNewRevision();
464 $comment = $rev->getComment( RevisionRecord::RAW );
465 $this->assertSame( '', $comment->text, 'comment text' );
466 }
467
468 public function provideSetUsePageCreationLog() {
469 yield [ true, [ [ 'create', 'create' ] ] ];
470 yield [ false, [] ];
471 }
472
473 /**
474 * @dataProvider provideSetUsePageCreationLog
475 * @param bool $use
476 */
477 public function testSetUsePageCreationLog( $use, $expected ) {
478 $user = $this->getTestUser()->getUser();
479 $title = $this->getDummyTitle( __METHOD__ . ( $use ? '_logged' : '_unlogged' ) );
480 $page = WikiPage::factory( $title );
481
482 $updater = $page->newPageUpdater( $user );
483 $updater->setUsePageCreationLog( $use );
484 $summary = CommentStoreComment::newUnsavedComment( 'cmt' );
485 $updater->setContent( 'main', new TextContent( 'Lorem Ipsum' ) );
486 $updater->saveRevision( $summary, EDIT_NEW );
487
488 $rev = $updater->getNewRevision();
489 $this->assertSelect(
490 'logging',
491 [ 'log_type', 'log_action' ],
492 [ 'log_page' => $rev->getPageId() ],
493 $expected
494 );
495 }
496
497 }