1<?php 2 3namespace MediaWiki\Tests\Storage; 4 5use CommentStoreComment; 6use Content; 7use DeferredUpdates; 8use FormatJson; 9use MediaWiki\Permissions\SimpleAuthority; 10use MediaWiki\Revision\RenderedRevision; 11use MediaWiki\Revision\RevisionRecord; 12use MediaWiki\Revision\SlotRecord; 13use MediaWiki\Storage\EditResult; 14use MediaWiki\User\UserIdentity; 15use MediaWikiIntegrationTestCase; 16use ParserOptions; 17use RecentChange; 18use Status; 19use TextContent; 20use Title; 21use User; 22use Wikimedia\AtEase\AtEase; 23use WikiPage; 24use WikitextContent; 25 26/** 27 * @covers \MediaWiki\Storage\PageUpdater 28 * @group Database 29 */ 30class PageUpdaterTest extends MediaWikiIntegrationTestCase { 31 32 private function newAuthority( UserIdentity $identity, $permissions = null ) { 33 if ( $permissions === null ) { 34 $permissions = [ 'view', 'edit', 'create' ]; 35 } 36 37 return new SimpleAuthority( $identity, $permissions ); 38 } 39 40 protected function setUp(): void { 41 parent::setUp(); 42 43 $slotRoleRegistry = $this->getServiceContainer()->getSlotRoleRegistry(); 44 45 if ( !$slotRoleRegistry->isDefinedRole( 'aux' ) ) { 46 $slotRoleRegistry->defineRoleWithModel( 47 'aux', 48 CONTENT_MODEL_WIKITEXT 49 ); 50 } 51 52 if ( !$slotRoleRegistry->isDefinedRole( 'derivedslot' ) ) { 53 $slotRoleRegistry->defineRoleWithModel( 54 'derivedslot', 55 CONTENT_MODEL_WIKITEXT, 56 [], 57 true 58 ); 59 } 60 61 $this->tablesUsed[] = 'logging'; 62 $this->tablesUsed[] = 'recentchanges'; 63 $this->tablesUsed[] = 'change_tag'; 64 } 65 66 private function getDummyTitle( $method ) { 67 return Title::newFromText( $method, $this->getDefaultWikitextNS() ); 68 } 69 70 /** 71 * @param int $revId 72 * 73 * @return null|RecentChange 74 */ 75 private function getRecentChangeFor( $revId ) { 76 $qi = RecentChange::getQueryInfo(); 77 $row = $this->db->selectRow( 78 $qi['tables'], 79 $qi['fields'], 80 [ 'rc_this_oldid' => $revId ], 81 __METHOD__, 82 [], 83 $qi['joins'] 84 ); 85 86 return $row ? RecentChange::newFromRow( $row ) : null; 87 } 88 89 /** 90 * @covers \MediaWiki\Storage\PageUpdater::saveRevision() 91 * @covers \WikiPage::newPageUpdater() 92 */ 93 public function testCreatePage() { 94 $user = $this->getTestUser()->getUser(); 95 $authority = $this->newAuthority( $user ); 96 97 $title = $this->getDummyTitle( __METHOD__ ); 98 $page = WikiPage::factory( $title ); 99 $updater = $page->newPageUpdater( $authority ); 100 101 $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' ); 102 103 $this->assertFalse( $updater->wasCommitted(), 'wasCommitted' ); 104 105 $updater->addTag( 'foo' ); 106 $updater->addTags( [ 'bar', 'qux' ] ); 107 108 $tags = $updater->getExplicitTags(); 109 sort( $tags ); 110 $this->assertSame( [ 'bar', 'foo', 'qux' ], $tags, 'getExplicitTags' ); 111 112 // TODO: MCR: test additional slots 113 $content = new TextContent( 'Lorem Ipsum' ); 114 $updater->setContent( SlotRecord::MAIN, $content ); 115 116 $parent = $updater->grabParentRevision(); 117 118 $this->assertNull( $parent, 'getParentRevision' ); 119 $this->assertFalse( $updater->wasCommitted(), 'wasCommitted' ); 120 121 // TODO: test that hasEditConflict() grabs the parent revision 122 $this->assertFalse( $updater->hasEditConflict( 0 ), 'hasEditConflict' ); 123 $this->assertTrue( $updater->hasEditConflict( 1 ), 'hasEditConflict' ); 124 125 // TODO: test failure with EDIT_UPDATE 126 // TODO: test EDIT_MINOR, EDIT_BOT, etc 127 $summary = CommentStoreComment::newUnsavedComment( 'Just a test' ); 128 $rev = $updater->saveRevision( $summary ); 129 130 $this->assertNotNull( $rev ); 131 $this->assertSame( 0, $rev->getParentId() ); 132 $this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text ); 133 $this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() ); 134 135 $this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' ); 136 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' ); 137 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' ); 138 $this->assertTrue( $updater->isNew(), 'isNew()' ); 139 $this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' ); 140 $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' ); 141 $this->assertInstanceOf( 142 RevisionRecord::class, 143 $updater->getStatus()->value['revision-record'] 144 ); 145 146 // check the EditResult object 147 $this->assertFalse( $updater->getEditResult()->getOriginalRevisionId(), 148 'EditResult::getOriginalRevisionId()' ); 149 $this->assertSame( 0, $updater->getEditResult()->getUndidRevId(), 150 'EditResult::getUndidRevId()' ); 151 $this->assertTrue( $updater->getEditResult()->isNew(), 'EditResult::isNew()' ); 152 $this->assertFalse( $updater->getEditResult()->isRevert(), 'EditResult::isRevert()' ); 153 154 $rev = $updater->getNewRevision(); 155 $revContent = $rev->getContent( SlotRecord::MAIN ); 156 $this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' ); 157 158 // were the WikiPage and Title objects updated? 159 $this->assertTrue( $page->exists(), 'WikiPage::exists()' ); 160 $this->assertTrue( $title->exists(), 'Title::exists()' ); 161 $this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getLatest()' ); 162 $this->assertNotNull( $page->getRevisionRecord(), 'WikiPage::getRevisionRecord()' ); 163 164 // re-load 165 $page2 = WikiPage::factory( $title ); 166 $this->assertTrue( $page2->exists(), 'WikiPage::exists()' ); 167 $this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getLatest()' ); 168 $this->assertNotNull( $page2->getRevisionRecord(), 'WikiPage::getRevisionRecord()' ); 169 170 // Check RC entry 171 $rc = $this->getRecentChangeFor( $rev->getId() ); 172 $this->assertNotNull( $rc, 'RecentChange' ); 173 174 // check site stats - this asserts that derived data updates where run. 175 $stats = $this->db->selectRow( 'site_stats', '*', '1=1' ); 176 $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages ); 177 $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits ); 178 179 // re-edit with same content - should be a "null-edit" 180 $updater = $page->newPageUpdater( $authority ); 181 $updater->setContent( SlotRecord::MAIN, $content ); 182 183 $summary = CommentStoreComment::newUnsavedComment( 'to to re-edit' ); 184 $rev = $updater->saveRevision( $summary ); 185 $status = $updater->getStatus(); 186 187 $this->assertNull( $rev, 'getNewRevision()' ); 188 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' ); 189 $this->assertTrue( $updater->isUnchanged(), 'isUnchanged' ); 190 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' ); 191 $this->assertTrue( $status->isOK(), 'getStatus()->isOK()' ); 192 $this->assertTrue( $status->hasMessage( 'edit-no-change' ), 'edit-no-change' ); 193 } 194 195 /** 196 * @covers \MediaWiki\Storage\PageUpdater::saveRevision() 197 * @covers \WikiPage::newPageUpdater() 198 */ 199 public function testUpdatePage() { 200 $user = $this->getTestUser()->getUser(); 201 $authority = $this->newAuthority( $user ); 202 203 $title = $this->getDummyTitle( __METHOD__ ); 204 $this->insertPage( $title ); 205 206 $page = WikiPage::factory( $title ); 207 $parentId = $page->getLatest(); 208 209 $updater = $page->newPageUpdater( $authority ); 210 211 $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' ); 212 213 $updater->setOriginalRevisionId( 7 ); 214 215 $this->assertFalse( $updater->hasEditConflict( $parentId ), 'hasEditConflict' ); 216 $this->assertTrue( $updater->hasEditConflict( $parentId - 1 ), 'hasEditConflict' ); 217 $this->assertTrue( $updater->hasEditConflict( 0 ), 'hasEditConflict' ); 218 219 // TODO: MCR: test additional slots 220 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) ); 221 222 // TODO: test all flags for saveRevision()! 223 $summary = CommentStoreComment::newUnsavedComment( 'Just a test' ); 224 $rev = $updater->saveRevision( $summary ); 225 226 $this->assertNotNull( $rev ); 227 $this->assertSame( $parentId, $rev->getParentId() ); 228 $this->assertSame( $summary->text, $rev->getComment( RevisionRecord::RAW )->text ); 229 $this->assertSame( $user->getName(), $rev->getUser( RevisionRecord::RAW )->getName() ); 230 231 $this->assertTrue( $updater->wasCommitted(), 'wasCommitted()' ); 232 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' ); 233 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' ); 234 $this->assertFalse( $updater->isNew(), 'isNew()' ); 235 $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' ); 236 $this->assertInstanceOf( 237 RevisionRecord::class, 238 $updater->getStatus()->value['revision-record'] 239 ); 240 $this->assertFalse( $updater->isUnchanged(), 'isUnchanged()' ); 241 242 // check the EditResult object 243 $this->assertSame( 7, $updater->getEditResult()->getOriginalRevisionId(), 244 'EditResult::getOriginalRevisionId()' ); 245 $this->assertSame( 0, $updater->getEditResult()->getUndidRevId(), 246 'EditResult::getUndidRevId()' ); 247 $this->assertFalse( $updater->getEditResult()->isNew(), 'EditResult::isNew()' ); 248 $this->assertFalse( $updater->getEditResult()->isRevert(), 'EditResult::isRevert()' ); 249 250 // TODO: Test null revision (with different user): new revision! 251 252 $rev = $updater->getNewRevision(); 253 $revContent = $rev->getContent( SlotRecord::MAIN ); 254 $this->assertSame( 'Lorem Ipsum', $revContent->serialize(), 'revision content' ); 255 256 // were the WikiPage and Title objects updated? 257 $this->assertTrue( $page->exists(), 'WikiPage::exists()' ); 258 $this->assertTrue( $title->exists(), 'Title::exists()' ); 259 $this->assertSame( $rev->getId(), $page->getLatest(), 'WikiPage::getLatest()' ); 260 $this->assertNotNull( $page->getRevisionRecord(), 'WikiPage::getRevisionRecord()' ); 261 262 // re-load 263 $page2 = WikiPage::factory( $title ); 264 $this->assertTrue( $page2->exists(), 'WikiPage::exists()' ); 265 $this->assertSame( $rev->getId(), $page2->getLatest(), 'WikiPage::getLatest()' ); 266 $this->assertNotNull( $page2->getRevisionRecord(), 'WikiPage::getRevisionRecord()' ); 267 268 // Check RC entry 269 $rc = $this->getRecentChangeFor( $rev->getId() ); 270 $this->assertNotNull( $rc, 'RecentChange' ); 271 272 // re-edit 273 $updater = $page->newPageUpdater( $authority ); 274 $updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) ); 275 276 $summary = CommentStoreComment::newUnsavedComment( 're-edit' ); 277 $updater->saveRevision( $summary ); 278 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' ); 279 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' ); 280 $this->assertNotNull( $updater->getNewRevision(), 'getNewRevision()' ); 281 282 $topRevisionId = $updater->getNewRevision()->getId(); 283 284 // perform a null edit 285 $updater = $page->newPageUpdater( $authority ); 286 $updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) ); 287 $summary = CommentStoreComment::newUnsavedComment( 'null edit' ); 288 $updater->saveRevision( $summary ); 289 290 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' ); 291 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' ); 292 $this->assertTrue( $updater->isUnchanged(), 'isUnchanged()' ); 293 $this->assertTrue( 294 $updater->getEditResult()->isNullEdit(), 295 'getEditResult()->isNullEdit()' 296 ); 297 $this->assertSame( 298 $topRevisionId, 299 $updater->getEditResult()->getOriginalRevisionId(), 300 'getEditResult()->getOriginalRevisionId()' 301 ); 302 303 // check site stats - this asserts that derived data updates where run. 304 $stats = $this->db->selectRow( 'site_stats', '*', '1=1' ); 305 $this->assertNotNull( $stats, 'site_stats' ); 306 $this->assertSame( $oldStats->ss_total_pages + 0, (int)$stats->ss_total_pages ); 307 $this->assertSame( $oldStats->ss_total_edits + 2, (int)$stats->ss_total_edits ); 308 } 309 310 public function testRevert() { 311 // Setup a page with some edits 312 $page = $this->getExistingTestPage( __METHOD__ ); 313 314 $user = $this->getTestUser()->getUser(); 315 $authority = $this->newAuthority( $user ); 316 317 $summary = CommentStoreComment::newUnsavedComment( '1' ); 318 $updater = $page->newPageUpdater( $authority ); 319 $updater->setContent( SlotRecord::MAIN, new TextContent( '1' ) ); 320 $updater->saveRevision( $summary ); 321 $revId1 = $updater->getNewRevision()->getId(); 322 323 $summary = CommentStoreComment::newUnsavedComment( '2' ); 324 $updater = $page->newPageUpdater( $authority ); 325 $updater->setContent( SlotRecord::MAIN, new TextContent( '2' ) ); 326 $updater->saveRevision( $summary ); 327 $revId2 = $updater->getNewRevision()->getId(); 328 329 // Perform a rollback 330 $updater = $page->newPageUpdater( $this->getTestSysop()->getUser() ); 331 $updater->setContent( SlotRecord::MAIN, new TextContent( '1' ) ); 332 $updater->markAsRevert( EditResult::REVERT_ROLLBACK, $revId2, $revId2 ); 333 $updater->setOriginalRevisionId( $revId1 ); 334 $summary = CommentStoreComment::newUnsavedComment( 'revert' ); 335 $updater->saveRevision( $summary ); 336 337 // Do some basic assertions on PageUpdater 338 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' ); 339 $this->assertTrue( $updater->getStatus()->isOK(), 'getStatus()->isOK()' ); 340 341 $editResult = $updater->getEditResult(); 342 $this->assertNotNull( $editResult, 'getEditResult()' ); 343 $this->assertTrue( $editResult->isRevert(), 'EditResult::isRevert()' ); 344 $this->assertTrue( $editResult->isExactRevert(), 'EditResult::isExactRevert()' ); 345 $this->assertSame( 346 $revId1, 347 $editResult->getOriginalRevisionId(), 348 'EditResult::getOriginalRevisionId()' 349 ); 350 $this->assertSame( 351 EditResult::REVERT_ROLLBACK, 352 $editResult->getRevertMethod(), 353 'EditResult::getRevertMethod()' 354 ); 355 $this->assertSame( 356 $revId2, 357 $editResult->getOldestRevertedRevisionId(), 358 'EditResult::getOldestRevertedRevisionId()' 359 ); 360 $this->assertSame( 361 $revId2, 362 $editResult->getNewestRevertedRevisionId(), 363 'EditResult::getNewestRevertedRevisionId()' 364 ); 365 366 // Ensure all deferred updates are run 367 DeferredUpdates::doUpdates(); 368 369 // Retrieve the mw-rollback change tag and verify it 370 $newRevId = $updater->getNewRevision()->getId(); 371 $this->assertSelect( 372 'change_tag', 373 'ct_params', 374 [ 'ct_rev_id' => $newRevId ], 375 [ [ FormatJson::encode( $editResult ) ] ] 376 ); 377 } 378 379 /** 380 * Creates a revision in the database. 381 * 382 * @param WikiPage $page 383 * @param string|Message|CommentStoreComment $summary 384 * @param null|string|Content $content 385 * 386 * @return RevisionRecord|null 387 */ 388 private function createRevision( WikiPage $page, $summary, $content = null ) { 389 $user = $this->getTestUser()->getUser(); 390 $authority = $this->newAuthority( $user ); 391 392 $comment = CommentStoreComment::newUnsavedComment( $summary ); 393 394 if ( !$content instanceof Content ) { 395 $content = new TextContent( $content ?? $summary ); 396 } 397 398 return $page->newPageUpdater( $authority ) 399 ->setContent( SlotRecord::MAIN, $content ) 400 ->saveRevision( $comment ); 401 } 402 403 /** 404 * Verify that MultiContentSave hook is called by saveRevision() with correct parameters. 405 * @covers \MediaWiki\Storage\PageUpdater::saveRevision() 406 */ 407 public function testMultiContentSaveHook() { 408 $user = $this->getTestUser()->getUser(); 409 $authority = $this->newAuthority( $user ); 410 411 $title = $this->getDummyTitle( __METHOD__ ); 412 413 // TODO: MCR: test additional slots 414 $slots = [ 415 SlotRecord::MAIN => new TextContent( 'Lorem Ipsum' ) 416 ]; 417 418 // start editing non-existing page 419 $page = WikiPage::factory( $title ); 420 $updater = $page->newPageUpdater( $authority ); 421 foreach ( $slots as $slot => $content ) { 422 $updater->setContent( $slot, $content ); 423 } 424 425 $summary = CommentStoreComment::newUnsavedComment( 'Just a test' ); 426 427 $expected = [ 428 'user' => $user, 429 'title' => $title, 430 'slots' => $slots, 431 'summary' => $summary 432 ]; 433 $hookFired = false; 434 $this->setTemporaryHook( 'MultiContentSave', 435 function ( RenderedRevision $renderedRevision, UserIdentity $user, 436 $summary, $flags, Status $hookStatus 437 ) use ( &$hookFired, $expected ) { 438 $hookFired = true; 439 440 $this->assertSame( $expected['summary'], $summary ); 441 $this->assertSame( EDIT_NEW, $flags ); 442 443 $this->assertTrue( 444 $expected['title']->isSamePageAs( $renderedRevision->getRevision()->getPage() ) 445 ); 446 $this->assertTrue( 447 $expected['title']->isSameLinkAs( $renderedRevision->getRevision()->getPageAsLinkTarget() ) 448 ); 449 450 $slots = $renderedRevision->getRevision()->getSlots(); 451 foreach ( $expected['slots'] as $slot => $content ) { 452 $this->assertSame( $content, $slots->getSlot( $slot )->getContent() ); 453 } 454 455 // Don't abort this edit. 456 return true; 457 } 458 ); 459 460 $rev = $updater->saveRevision( $summary ); 461 $this->assertTrue( $hookFired, "MultiContentSave hook wasn't called." ); 462 $this->assertNotNull( $rev, 463 "MultiContentSave returned true, but revision wasn't created." ); 464 } 465 466 /** 467 * Verify that MultiContentSave hook can abort saveRevision() by returning false. 468 * @covers \MediaWiki\Storage\PageUpdater::saveRevision() 469 */ 470 public function testMultiContentSaveHookAbort() { 471 $user = $this->getTestUser()->getUser(); 472 $authority = $this->newAuthority( $user ); 473 $title = $this->getDummyTitle( __METHOD__ ); 474 475 // start editing non-existing page 476 $page = WikiPage::factory( $title ); 477 $updater = $page->newPageUpdater( $authority ); 478 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) ); 479 480 $summary = CommentStoreComment::newUnsavedComment( 'Just a test' ); 481 482 $expectedError = 'aborted-by-test-hook'; 483 $this->setTemporaryHook( 'MultiContentSave', 484 static function ( RenderedRevision $renderedRevision, UserIdentity $user, 485 $summary, $flags, Status $hookStatus 486 ) use ( $expectedError ) { 487 $hookStatus->fatal( $expectedError ); 488 489 // Returning false should disallow saveRevision() to continue saving this revision. 490 return false; 491 } 492 ); 493 494 $rev = $updater->saveRevision( $summary ); 495 $this->assertNull( $rev, 496 "MultiContentSave returned false, but revision was still created." ); 497 498 $status = $updater->getStatus(); 499 $this->assertFalse( $status->isOK(), 500 "MultiContentSave returned false, but Status is not fatal." ); 501 $this->assertSame( $expectedError, $status->getMessage()->getKey() ); 502 } 503 504 /** 505 * @covers \MediaWiki\Storage\PageUpdater::grabParentRevision() 506 * @covers \MediaWiki\Storage\PageUpdater::saveRevision() 507 */ 508 public function testCompareAndSwapFailure() { 509 $user = $this->getTestUser()->getUser(); 510 $authority = $this->newAuthority( $user ); 511 512 $title = $this->getDummyTitle( __METHOD__ ); 513 514 // start editing non-existing page 515 $page = WikiPage::factory( $title ); 516 $updater = $page->newPageUpdater( $authority ); 517 $updater->grabParentRevision(); 518 519 // create page concurrently 520 $concurrentPage = WikiPage::factory( $title ); 521 $this->createRevision( $concurrentPage, __METHOD__ . '-one' ); 522 523 // try creating the page - should trigger CAS failure. 524 $summary = CommentStoreComment::newUnsavedComment( 'create?!' ); 525 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) ); 526 $updater->saveRevision( $summary ); 527 $status = $updater->getStatus(); 528 529 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' ); 530 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' ); 531 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' ); 532 $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-conflict' ); 533 534 // start editing existing page 535 $page = WikiPage::factory( $title ); 536 $updater = $page->newPageUpdater( $authority ); 537 $updater->grabParentRevision(); 538 539 // update page concurrently 540 $concurrentPage = WikiPage::factory( $title ); 541 $this->createRevision( $concurrentPage, __METHOD__ . '-two' ); 542 543 // try creating the page - should trigger CAS failure. 544 $summary = CommentStoreComment::newUnsavedComment( 'edit?!' ); 545 $updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) ); 546 $updater->saveRevision( $summary ); 547 $status = $updater->getStatus(); 548 549 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' ); 550 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' ); 551 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' ); 552 $this->assertTrue( $status->hasMessage( 'edit-conflict' ), 'edit-conflict' ); 553 } 554 555 /** 556 * @covers \MediaWiki\Storage\PageUpdater::saveRevision() 557 */ 558 public function testFailureOnEditFlags() { 559 $user = $this->getTestUser()->getUser(); 560 $authority = $this->newAuthority( $user ); 561 562 $title = $this->getDummyTitle( __METHOD__ ); 563 564 // start editing non-existing page 565 $page = WikiPage::factory( $title ); 566 $updater = $page->newPageUpdater( $authority ); 567 568 // update with EDIT_UPDATE flag should fail 569 $summary = CommentStoreComment::newUnsavedComment( 'udpate?!' ); 570 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) ); 571 $updater->saveRevision( $summary, EDIT_UPDATE ); 572 $status = $updater->getStatus(); 573 574 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' ); 575 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' ); 576 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' ); 577 $this->assertTrue( $status->hasMessage( 'edit-gone-missing' ), 'edit-gone-missing' ); 578 579 // create the page 580 $this->createRevision( $page, __METHOD__ ); 581 582 // update with EDIT_NEW flag should fail 583 $summary = CommentStoreComment::newUnsavedComment( 'create?!' ); 584 $updater = $page->newPageUpdater( $authority ); 585 $updater->setContent( SlotRecord::MAIN, new TextContent( 'dolor sit amet' ) ); 586 $updater->saveRevision( $summary, EDIT_NEW ); 587 $status = $updater->getStatus(); 588 589 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' ); 590 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' ); 591 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' ); 592 $this->assertTrue( $status->hasMessage( 'edit-already-exists' ), 'edit-already-exists' ); 593 } 594 595 /** 596 * @covers \MediaWiki\Storage\PageUpdater::saveRevision() 597 */ 598 public function testFailureOnBadContentModel() { 599 $user = $this->getTestUser()->getUser(); 600 $authority = $this->newAuthority( $user ); 601 602 $title = $this->getDummyTitle( __METHOD__ ); 603 604 // start editing non-existing page 605 $page = WikiPage::factory( $title ); 606 $updater = $page->newPageUpdater( $authority ); 607 608 // plain text content should fail in aux slot (the main slot doesn't care) 609 $updater->setContent( 'main', new TextContent( 'Main Content' ) ); 610 $updater->setContent( 'aux', new TextContent( 'Aux Content' ) ); 611 612 $summary = CommentStoreComment::newUnsavedComment( 'udpate?!' ); 613 $updater->saveRevision( $summary, EDIT_UPDATE ); 614 $status = $updater->getStatus(); 615 616 $this->assertFalse( $updater->wasSuccessful(), 'wasSuccessful()' ); 617 $this->assertNull( $updater->getNewRevision(), 'getNewRevision()' ); 618 $this->assertFalse( $status->isOK(), 'getStatus()->isOK()' ); 619 $this->assertTrue( 620 $status->hasMessage( 'content-not-allowed-here' ), 621 'content-not-allowed-here' 622 ); 623 } 624 625 public function provideSetRcPatrolStatus( $patrolled ) { 626 yield [ RecentChange::PRC_UNPATROLLED ]; 627 yield [ RecentChange::PRC_AUTOPATROLLED ]; 628 } 629 630 /** 631 * @dataProvider provideSetRcPatrolStatus 632 * @covers \MediaWiki\Storage\PageUpdater::setRcPatrolStatus() 633 */ 634 public function testSetRcPatrolStatus( $patrolled ) { 635 $revisionStore = $this->getServiceContainer()->getRevisionStore(); 636 637 $user = $this->getTestUser()->getUser(); 638 $authority = $this->newAuthority( $user ); 639 640 $title = $this->getDummyTitle( __METHOD__ ); 641 642 $page = WikiPage::factory( $title ); 643 644 $summary = CommentStoreComment::newUnsavedComment( 'Lorem ipsum ' . $patrolled ); 645 $rev = $page->newPageUpdater( $authority ) 646 ->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum ' . $patrolled ) ) 647 ->setRcPatrolStatus( $patrolled ) 648 ->saveRevision( $summary ); 649 650 $rc = $revisionStore->getRecentChange( $rev ); 651 $this->assertEquals( $patrolled, $rc->getAttribute( 'rc_patrolled' ) ); 652 } 653 654 /** 655 * @covers \MediaWiki\Storage\PageUpdater::makeNewRevision() 656 */ 657 public function testStalePageID() { 658 $user = $this->getTestUser()->getUser(); 659 $authority = $this->newAuthority( $user ); 660 661 $title = $this->getDummyTitle( __METHOD__ ); 662 $summary = CommentStoreComment::newUnsavedComment( 'testing...' ); 663 664 // Create page 665 $page = WikiPage::factory( $title ); 666 $updater = $page->newPageUpdater( $authority ); 667 $updater->setContent( 'main', new TextContent( 'Content 1' ) ); 668 $updater->saveRevision( $summary, EDIT_NEW ); 669 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' ); 670 671 // Create a clone of $title and $page. 672 $title = Title::makeTitle( $title->getNamespace(), $title->getDBkey() ); 673 $page = WikiPage::factory( $title ); 674 675 // start editing existing page using bad page ID 676 $updater = $page->newPageUpdater( $authority ); 677 $updater->grabParentRevision(); 678 679 $updater->setContent( 'main', new TextContent( 'Content 2' ) ); 680 681 // Force the article ID to something invalid, 682 // to emulate confusion due to a page move. 683 $title->resetArticleID( 886655 ); 684 685 AtEase::suppressWarnings(); 686 $updater->saveRevision( $summary, EDIT_UPDATE ); 687 AtEase::restoreWarnings(); 688 689 $this->assertTrue( $updater->wasSuccessful(), 'wasSuccessful()' ); 690 } 691 692 /** 693 * @covers \MediaWiki\Storage\PageUpdater::inheritSlot() 694 * @covers \MediaWiki\Storage\PageUpdater::setContent() 695 */ 696 public function testInheritSlot() { 697 $user = $this->getTestUser()->getUser(); 698 $authority = $this->newAuthority( $user ); 699 700 $title = $this->getDummyTitle( __METHOD__ ); 701 $page = WikiPage::factory( $title ); 702 703 $updater = $page->newPageUpdater( $authority ); 704 $summary = CommentStoreComment::newUnsavedComment( 'one' ); 705 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) ); 706 $rev1 = $updater->saveRevision( $summary, EDIT_NEW ); 707 708 $updater = $page->newPageUpdater( $authority ); 709 $summary = CommentStoreComment::newUnsavedComment( 'two' ); 710 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Foo Bar' ) ); 711 $rev2 = $updater->saveRevision( $summary, EDIT_UPDATE ); 712 713 $updater = $page->newPageUpdater( $authority ); 714 $summary = CommentStoreComment::newUnsavedComment( 'three' ); 715 $updater->inheritSlot( $rev1->getSlot( SlotRecord::MAIN ) ); 716 $rev3 = $updater->saveRevision( $summary, EDIT_UPDATE ); 717 718 $this->assertNotSame( $rev1->getId(), $rev3->getId() ); 719 $this->assertNotSame( $rev2->getId(), $rev3->getId() ); 720 721 $main1 = $rev1->getSlot( SlotRecord::MAIN ); 722 $main3 = $rev3->getSlot( SlotRecord::MAIN ); 723 724 $this->assertNotSame( $main1->getRevision(), $main3->getRevision() ); 725 $this->assertSame( $main1->getAddress(), $main3->getAddress() ); 726 $this->assertTrue( $main1->getContent()->equals( $main3->getContent() ) ); 727 } 728 729 /** 730 * @covers \MediaWiki\Storage\PageUpdater::setSlot() 731 * @covers \MediaWiki\Storage\PageUpdater::updateRevision() 732 */ 733 public function testUpdatingDerivedSlot() { 734 $user = $this->getTestUser()->getUser(); 735 $title = $this->getDummyTitle( __METHOD__ ); 736 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title ); 737 738 $updater = $page->newPageUpdater( $user ); 739 $summary = CommentStoreComment::newUnsavedComment( 'one' ); 740 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) ); 741 $updater->saveRevision( $summary, EDIT_NEW ); 742 743 $updater = $page->newPageUpdater( $user ); 744 $content = new WikitextContent( 'A' ); 745 $derived = SlotRecord::newDerived( 'derivedslot', $content ); 746 $updater->setSlot( $derived ); 747 $updater->updateRevision(); 748 749 $status = $updater->getStatus(); 750 $this->assertTrue( $status->isOK() ); 751 $rev = $status->getValue()['revision-record']; 752 $slot = $rev->getSlot( 'derivedslot' ); 753 $this->assertTrue( $slot->getContent()->equals( $content ) ); 754 } 755 756 /** 757 * @covers \MediaWiki\Storage\PageUpdater::setSlot() 758 * @covers \MediaWiki\Storage\PageUpdater::updateRevision() 759 */ 760 public function testUpdatingDerivedSlotCurrentRevision() { 761 $user = $this->getTestUser()->getUser(); 762 $title = $this->getDummyTitle( __METHOD__ ); 763 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title ); 764 765 $updater = $page->newPageUpdater( $user ); 766 $summary = CommentStoreComment::newUnsavedComment( 'one' ); 767 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) ); 768 $rev1 = $updater->saveRevision( $summary, EDIT_NEW ); 769 770 $updater = $page->newPageUpdater( $user ); 771 $content = new WikitextContent( 'A' ); 772 $derived = SlotRecord::newDerived( 'derivedslot', $content ); 773 $updater->setSlot( $derived ); 774 $updater->updateRevision( $rev1->getId( $rev1->getWikiId() ) ); 775 776 $rev2 = $updater->getStatus()->getValue()['revision-record']; 777 $slot = $rev2->getSlot( 'derivedslot' ); 778 $this->assertTrue( $slot->getContent()->equals( $content ) ); 779 } 780 781 /** 782 * @covers \MediaWiki\Storage\PageUpdater::setSlot() 783 * @covers \MediaWiki\Storage\PageUpdater::updateRevision() 784 */ 785 public function testUpdatingDerivedSlotOldRevision() { 786 $user = $this->getTestUser()->getUser(); 787 $title = $this->getDummyTitle( __METHOD__ ); 788 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title ); 789 790 $updater = $page->newPageUpdater( $user ); 791 $summary = CommentStoreComment::newUnsavedComment( 'one' ); 792 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Lorem ipsum' ) ); 793 $rev1 = $updater->saveRevision( $summary, EDIT_NEW ); 794 795 $updater = $page->newPageUpdater( $user ); 796 $summary = CommentStoreComment::newUnsavedComment( 'two' ); 797 $updater->setContent( SlotRecord::MAIN, new TextContent( 'Something different' ) ); 798 $updater->saveRevision( $summary, EDIT_UPDATE ); 799 800 $updater = $page->newPageUpdater( $user ); 801 $content = new WikitextContent( 'A' ); 802 $derived = SlotRecord::newDerived( 'derivedslot', $content ); 803 $updater->setSlot( $derived ); 804 $updater->updateRevision( $rev1->getId( $rev1->getWikiId() ) ); 805 806 $rev3 = $updater->getStatus()->getValue()['revision-record']; 807 $slot = $rev3->getSlot( 'derivedslot' ); 808 $this->assertTrue( $slot->getContent()->equals( $content ) ); 809 } 810 811 // TODO: MCR: test adding multiple slots, inheriting parent slots, and removing slots. 812 813 public function testSetUseAutomaticEditSummaries() { 814 $this->setContentLang( 'qqx' ); 815 $user = $this->getTestUser()->getUser(); 816 $authority = $this->newAuthority( $user ); 817 818 $title = $this->getDummyTitle( __METHOD__ ); 819 $page = WikiPage::factory( $title ); 820 821 $updater = $page->newPageUpdater( $authority ) 822 ->setUseAutomaticEditSummaries( true ) 823 ->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) ); 824 825 // empty comment triggers auto-summary 826 $summary = CommentStoreComment::newUnsavedComment( '' ); 827 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY ); 828 829 $rev = $updater->getNewRevision(); 830 $comment = $rev->getComment( RevisionRecord::RAW ); 831 $this->assertSame( '(autosumm-new: Lorem Ipsum)', $comment->text, 'comment text' ); 832 833 // check that this also works when blanking the page 834 $updater = $page->newPageUpdater( $authority ) 835 ->setUseAutomaticEditSummaries( true ) 836 ->setContent( SlotRecord::MAIN, new TextContent( '' ) ); 837 838 $summary = CommentStoreComment::newUnsavedComment( '' ); 839 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY ); 840 841 $rev = $updater->getNewRevision(); 842 $comment = $rev->getComment( RevisionRecord::RAW ); 843 $this->assertSame( '(autosumm-blank)', $comment->text, 'comment text' ); 844 845 // check that we can also disable edit-summaries 846 $title2 = $this->getDummyTitle( __METHOD__ . '/2' ); 847 $page2 = WikiPage::factory( $title2 ); 848 849 $updater = $page2->newPageUpdater( $authority ) 850 ->setUseAutomaticEditSummaries( false ) 851 ->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) ); 852 853 $summary = CommentStoreComment::newUnsavedComment( '' ); 854 $updater->saveRevision( $summary, EDIT_AUTOSUMMARY ); 855 856 $rev = $updater->getNewRevision(); 857 $comment = $rev->getComment( RevisionRecord::RAW ); 858 $this->assertSame( '', $comment->text, 'comment text should still be blank' ); 859 860 // check that we don't do auto.summaries without the EDIT_AUTOSUMMARY flag 861 $updater = $page2->newPageUpdater( $authority ) 862 ->setUseAutomaticEditSummaries( true ) 863 ->setContent( SlotRecord::MAIN, new TextContent( '' ) ); 864 865 $summary = CommentStoreComment::newUnsavedComment( '' ); 866 $updater->saveRevision( $summary, 0 ); 867 868 $rev = $updater->getNewRevision(); 869 $comment = $rev->getComment( RevisionRecord::RAW ); 870 $this->assertSame( '', $comment->text, 'comment text' ); 871 } 872 873 public function provideSetUsePageCreationLog() { 874 yield [ true, [ [ 'create', 'create' ] ] ]; 875 yield [ false, [] ]; 876 } 877 878 /** 879 * @dataProvider provideSetUsePageCreationLog 880 */ 881 public function testSetUsePageCreationLog( $use, $expected ) { 882 $user = $this->getTestUser()->getUser(); 883 $authority = $this->newAuthority( $user ); 884 885 $title = $this->getDummyTitle( __METHOD__ . ( $use ? '_logged' : '_unlogged' ) ); 886 $page = WikiPage::factory( $title ); 887 888 $summary = CommentStoreComment::newUnsavedComment( 'cmt' ); 889 $updater = $page->newPageUpdater( $authority ) 890 ->setUsePageCreationLog( $use ) 891 ->setContent( SlotRecord::MAIN, new TextContent( 'Lorem Ipsum' ) ); 892 $updater->saveRevision( $summary, EDIT_NEW ); 893 894 $rev = $updater->getNewRevision(); 895 $this->assertSelect( 896 'logging', 897 [ 'log_type', 'log_action' ], 898 [ 'log_page' => $rev->getPageId() ], 899 $expected 900 ); 901 } 902 903 public function provideMagicWords() { 904 yield 'PAGEID' => [ 905 'Test {{PAGEID}} Test', 906 static function ( RevisionRecord $rev ) { 907 return $rev->getPageId(); 908 } 909 ]; 910 911 yield 'REVISIONID' => [ 912 'Test {{REVISIONID}} Test', 913 static function ( RevisionRecord $rev ) { 914 return $rev->getId(); 915 } 916 ]; 917 918 yield 'REVISIONUSER' => [ 919 'Test {{REVISIONUSER}} Test', 920 static function ( RevisionRecord $rev ) { 921 return $rev->getUser()->getName(); 922 } 923 ]; 924 925 yield 'REVISIONTIMESTAMP' => [ 926 'Test {{REVISIONTIMESTAMP}} Test', 927 static function ( RevisionRecord $rev ) { 928 return $rev->getTimestamp(); 929 } 930 ]; 931 932 yield 'subst:REVISIONUSER' => [ 933 'Test {{subst:REVISIONUSER}} Test', 934 static function ( RevisionRecord $rev ) { 935 return $rev->getUser()->getName(); 936 }, 937 'subst' 938 ]; 939 940 yield 'subst:PAGENAME' => [ 941 'Test {{subst:PAGENAME}} Test', 942 static function ( RevisionRecord $rev ) { 943 return 'PageUpdaterTest::testMagicWords'; 944 }, 945 'subst' 946 ]; 947 } 948 949 /** 950 * @covers \MediaWiki\Storage\PageUpdater::saveRevision() 951 * 952 * Integration test for PageUpdater, DerivedPageDataUpdater, RevisionRenderer 953 * and RenderedRevision, that ensures that magic words depending on revision meta-data 954 * are handled correctly. Note that each magic word needs to be tested separately, 955 * to assert correct behavior for each "vary" flag in the ParserOutput. 956 * 957 * @dataProvider provideMagicWords 958 */ 959 public function testMagicWords( $wikitext, $callback, $subst = false ) { 960 $user = User::newFromName( 'A user for ' . __METHOD__ ); 961 $user->addToDatabase(); 962 963 $authority = $this->newAuthority( $user ); 964 965 $title = $this->getDummyTitle( __METHOD__ . '-' . $this->getName() ); 966 $this->insertPage( $title ); 967 968 $page = WikiPage::factory( $title ); 969 $updater = $page->newPageUpdater( $authority ) 970 ->setContent( SlotRecord::MAIN, new \WikitextContent( $wikitext ) ); 971 972 $summary = CommentStoreComment::newUnsavedComment( 'Just a test' ); 973 $rev = $updater->saveRevision( $summary, EDIT_UPDATE ); 974 975 if ( !$rev ) { 976 $this->fail( $updater->getStatus()->getWikiText() ); 977 } 978 979 $expected = strval( $callback( $rev ) ); 980 981 $output = $page->getParserOutput( ParserOptions::newCanonical( 'canonical' ) ); 982 $html = $output->getText(); 983 $text = $rev->getContent( SlotRecord::MAIN )->serialize(); 984 985 if ( $subst ) { 986 $this->assertStringContainsString( $expected, $text, 'In Wikitext' ); 987 } 988 989 $this->assertStringContainsString( $expected, $html, 'In HTML' ); 990 } 991 992} 993