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