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