1<?php 2 3namespace MediaWiki\Tests\Storage; 4 5use BagOStuff; 6use CommentStoreComment; 7use Content; 8use ContentHandler; 9use DeferredUpdates; 10use DummyContentHandlerForTesting; 11use JobQueueGroup; 12use LinksUpdate; 13use MediaWiki\Config\ServiceOptions; 14use MediaWiki\MediaWikiServices; 15use MediaWiki\Revision\MutableRevisionRecord; 16use MediaWiki\Revision\MutableRevisionSlots; 17use MediaWiki\Revision\RevisionRecord; 18use MediaWiki\Revision\SlotRecord; 19use MediaWiki\Storage\DerivedPageDataUpdater; 20use MediaWiki\Storage\EditResult; 21use MediaWiki\Storage\EditResultCache; 22use MediaWiki\Storage\RevisionSlotsUpdate; 23use MediaWikiIntegrationTestCase; 24use MockTitleTrait; 25use MWCallableUpdate; 26use MWTimestamp; 27use PHPUnit\Framework\MockObject\MockObject; 28use TextContent; 29use TextContentHandler; 30use Title; 31use User; 32use Wikimedia\TestingAccessWrapper; 33use WikiPage; 34use WikitextContent; 35use WikitextContentHandler; 36 37/** 38 * @group Database 39 * 40 * @covers \MediaWiki\Storage\DerivedPageDataUpdater 41 */ 42class DerivedPageDataUpdaterTest extends MediaWikiIntegrationTestCase { 43 use MockTitleTrait; 44 45 protected function setUp(): void { 46 parent::setUp(); 47 48 $this->tablesUsed[] = 'page'; 49 } 50 51 /** 52 * @param string $title 53 * 54 * @return Title 55 */ 56 private function getTitle( $title ) { 57 return Title::makeTitleSafe( $this->getDefaultWikitextNS(), $title ); 58 } 59 60 /** 61 * @param string|Title $title 62 * 63 * @return WikiPage 64 */ 65 private function getPage( $title ) { 66 $title = ( $title instanceof Title ) ? $title : $this->getTitle( $title ); 67 68 return WikiPage::factory( $title ); 69 } 70 71 /** 72 * @param string|Title|WikiPage $page 73 * @param RevisionRecord|null $rec 74 * @param User|null $user 75 * 76 * @return DerivedPageDataUpdater 77 */ 78 private function getDerivedPageDataUpdater( 79 $page, RevisionRecord $rec = null, User $user = null 80 ) { 81 if ( is_string( $page ) || $page instanceof Title ) { 82 $page = $this->getPage( $page ); 83 } 84 85 $page = TestingAccessWrapper::newFromObject( $page ); 86 return $page->getDerivedDataUpdater( $user, $rec ); 87 } 88 89 /** 90 * Creates a revision in the database. 91 * 92 * @param WikiPage $page 93 * @param string|Message|CommentStoreComment $summary 94 * @param null|string|Content $content 95 * @param User|null $user 96 * 97 * @return RevisionRecord|null 98 */ 99 private function createRevision( WikiPage $page, $summary, $content = null, $user = null ) { 100 $user = $user ?: $this->getTestUser()->getUser(); 101 $comment = CommentStoreComment::newUnsavedComment( $summary ); 102 103 if ( $content === null || is_string( $content ) ) { 104 $content = new WikitextContent( $content ?? $summary ); 105 } 106 107 if ( !is_array( $content ) ) { 108 $content = [ 'main' => $content ]; 109 } 110 111 $this->getDerivedPageDataUpdater( $page ); // flush cached instance before. 112 113 $updater = $page->newPageUpdater( $user ); 114 115 foreach ( $content as $role => $c ) { 116 $updater->setContent( $role, $c ); 117 } 118 119 $rev = $updater->saveRevision( $comment ); 120 if ( !$updater->wasSuccessful() ) { 121 $this->fail( $updater->getStatus()->getWikiText() ); 122 } 123 124 $this->getDerivedPageDataUpdater( $page ); // flush cached instance after. 125 return $rev; 126 } 127 128 // TODO: test setArticleCountMethod() and isCountable(); 129 // TODO: test isRedirect() and wasRedirect() 130 131 /** 132 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOptions() 133 */ 134 public function testGetCanonicalParserOptions() { 135 $user = $this->getTestUser()->getUser(); 136 $page = $this->getPage( __METHOD__ ); 137 138 $parentRev = $this->createRevision( $page, 'first' ); 139 140 $mainContent = new WikitextContent( 'Lorem ipsum' ); 141 142 $update = new RevisionSlotsUpdate(); 143 $update->modifyContent( SlotRecord::MAIN, $mainContent ); 144 $updater = $this->getDerivedPageDataUpdater( $page ); 145 $updater->prepareContent( $user, $update, false ); 146 147 $options1 = $updater->getCanonicalParserOptions(); 148 $this->assertSame( MediaWikiServices::getInstance()->getContentLanguage(), 149 $options1->getUserLangObj() ); 150 151 $speculativeId = $options1->getSpeculativeRevId(); 152 $this->assertSame( $parentRev->getId() + 1, $speculativeId ); 153 154 $rev = $this->makeRevision( 155 $page->getTitle(), 156 $update, 157 $user, 158 $parentRev->getId() + 7, 159 $parentRev->getId() 160 ); 161 $updater->prepareUpdate( $rev ); 162 163 $options2 = $updater->getCanonicalParserOptions(); 164 165 $currentRev = $options2->getCurrentRevisionRecordCallback()( $page->getTitle() ); 166 $this->assertSame( $rev->getId(), $currentRev->getId() ); 167 } 168 169 /** 170 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::grabCurrentRevision() 171 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted() 172 */ 173 public function testGrabCurrentRevision() { 174 $page = $this->getPage( __METHOD__ ); 175 176 $updater0 = $this->getDerivedPageDataUpdater( $page ); 177 $this->assertNull( $updater0->grabCurrentRevision() ); 178 $this->assertFalse( $updater0->pageExisted() ); 179 180 $rev1 = $this->createRevision( $page, 'first' ); 181 $updater1 = $this->getDerivedPageDataUpdater( $page ); 182 $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() ); 183 $this->assertFalse( $updater0->pageExisted() ); 184 $this->assertTrue( $updater1->pageExisted() ); 185 186 $rev2 = $this->createRevision( $page, 'second' ); 187 $updater2 = $this->getDerivedPageDataUpdater( $page ); 188 $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() ); 189 $this->assertSame( $rev2->getId(), $updater2->grabCurrentRevision()->getId() ); 190 } 191 192 /** 193 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent() 194 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isContentPrepared() 195 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted() 196 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation() 197 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange() 198 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots() 199 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot() 200 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent() 201 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles() 202 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles() 203 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput() 204 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput() 205 */ 206 public function testPrepareContent() { 207 $slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry(); 208 if ( !$slotRoleRegistry->isDefinedRole( 'aux' ) ) { 209 $slotRoleRegistry->defineRoleWithModel( 210 'aux', 211 CONTENT_MODEL_WIKITEXT 212 ); 213 } 214 215 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); 216 $updater = $this->getDerivedPageDataUpdater( __METHOD__ ); 217 218 $this->assertFalse( $updater->isContentPrepared() ); 219 220 // TODO: test stash 221 // TODO: MCR: Test multiple slots. Test slot removal. 222 $mainContent = new WikitextContent( 'first [[main]] ~~~' ); 223 $auxContent = new WikitextContent( 'inherited ~~~ content' ); 224 $auxSlot = SlotRecord::newSaved( 225 10, 7, 'tt:7', 226 SlotRecord::newUnsaved( 'aux', $auxContent ) 227 ); 228 229 $update = new RevisionSlotsUpdate(); 230 $update->modifyContent( SlotRecord::MAIN, $mainContent ); 231 $update->modifySlot( SlotRecord::newInherited( $auxSlot ) ); 232 // TODO: MCR: test removing slots! 233 234 $updater->prepareContent( $sysop, $update, false ); 235 236 // second be ok to call again with the same params 237 $updater->prepareContent( $sysop, $update, false ); 238 239 $this->assertNull( $updater->grabCurrentRevision() ); 240 $this->assertTrue( $updater->isContentPrepared() ); 241 $this->assertFalse( $updater->isUpdatePrepared() ); 242 $this->assertFalse( $updater->pageExisted() ); 243 $this->assertTrue( $updater->isCreation() ); 244 $this->assertTrue( $updater->isChange() ); 245 $this->assertFalse( $updater->isContentDeleted() ); 246 247 $this->assertNotNull( $updater->getRevision() ); 248 $this->assertNotNull( $updater->getRenderedRevision() ); 249 250 $this->assertEquals( [ 'main', 'aux' ], $updater->getSlots()->getSlotRoles() ); 251 $this->assertEquals( [ 'main' ], array_keys( $updater->getSlots()->getOriginalSlots() ) ); 252 $this->assertEquals( [ 'aux' ], array_keys( $updater->getSlots()->getInheritedSlots() ) ); 253 $this->assertEquals( [ 'main', 'aux' ], $updater->getModifiedSlotRoles() ); 254 $this->assertEquals( [ 'main', 'aux' ], $updater->getTouchedSlotRoles() ); 255 256 $mainSlot = $updater->getRawSlot( SlotRecord::MAIN ); 257 $this->assertInstanceOf( SlotRecord::class, $mainSlot ); 258 $this->assertStringNotContainsString( 259 '~~~', 260 $mainSlot->getContent()->serialize(), 261 'PST should apply.' 262 ); 263 $this->assertStringContainsString( $sysop->getName(), $mainSlot->getContent()->serialize() ); 264 265 $auxSlot = $updater->getRawSlot( 'aux' ); 266 $this->assertInstanceOf( SlotRecord::class, $auxSlot ); 267 $this->assertStringContainsString( 268 '~~~', 269 $auxSlot->getContent()->serialize(), 270 'No PST should apply.' 271 ); 272 273 $mainOutput = $updater->getCanonicalParserOutput(); 274 $this->assertStringContainsString( 'first', $mainOutput->getText() ); 275 $this->assertStringContainsString( '<a ', $mainOutput->getText() ); 276 $this->assertNotEmpty( $mainOutput->getLinks() ); 277 278 $canonicalOutput = $updater->getCanonicalParserOutput(); 279 $this->assertStringContainsString( 'first', $canonicalOutput->getText() ); 280 $this->assertStringContainsString( '<a ', $canonicalOutput->getText() ); 281 $this->assertStringContainsString( 'inherited ', $canonicalOutput->getText() ); 282 $this->assertNotEmpty( $canonicalOutput->getLinks() ); 283 } 284 285 /** 286 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent() 287 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted() 288 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation() 289 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange() 290 */ 291 public function testPrepareContentInherit() { 292 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser(); 293 $page = $this->getPage( __METHOD__ ); 294 295 $mainContent1 = new WikitextContent( 'first [[main]] ({{REVISIONUSER}}) #~~~#' ); 296 $mainContent2 = new WikitextContent( 'second ({{subst:REVISIONUSER}}) #~~~#' ); 297 298 $rev = $this->createRevision( $page, 'first', $mainContent1 ); 299 $mainContent1 = $rev->getContent( SlotRecord::MAIN ); // get post-pst content 300 $userName = $rev->getUser()->getName(); 301 $sysopName = $sysop->getName(); 302 303 $update = new RevisionSlotsUpdate(); 304 $update->modifyContent( SlotRecord::MAIN, $mainContent1 ); 305 $updater1 = $this->getDerivedPageDataUpdater( $page ); 306 $updater1->prepareContent( $sysop, $update, false ); 307 308 $this->assertNotNull( $updater1->grabCurrentRevision() ); 309 $this->assertTrue( $updater1->isContentPrepared() ); 310 $this->assertTrue( $updater1->pageExisted() ); 311 $this->assertFalse( $updater1->isCreation() ); 312 $this->assertFalse( $updater1->isChange() ); 313 314 $this->assertNotNull( $updater1->getRevision() ); 315 $this->assertNotNull( $updater1->getRenderedRevision() ); 316 317 // parser-output for null-edit uses the original author's name 318 $html = $updater1->getRenderedRevision()->getRevisionParserOutput()->getText(); 319 $this->assertStringNotContainsString( $sysopName, $html, '{{REVISIONUSER}}' ); 320 $this->assertStringNotContainsString( '{{REVISIONUSER}}', $html, '{{REVISIONUSER}}' ); 321 $this->assertStringNotContainsString( '~~~', $html, 'signature ~~~' ); 322 $this->assertStringContainsString( '(' . $userName . ')', $html, '{{REVISIONUSER}}' ); 323 $this->assertStringContainsString( '>' . $userName . '<', $html, 'signature ~~~' ); 324 325 // TODO: MCR: test inheritance from parent 326 $update = new RevisionSlotsUpdate(); 327 $update->modifyContent( SlotRecord::MAIN, $mainContent2 ); 328 $updater2 = $this->getDerivedPageDataUpdater( $page ); 329 $updater2->prepareContent( $sysop, $update, false ); 330 331 // non-null edit use the new user name in PST 332 $pstText = $updater2->getSlots()->getContent( SlotRecord::MAIN )->serialize(); 333 $this->assertStringNotContainsString( 334 '{{subst:REVISIONUSER}}', 335 $pstText, 336 '{{subst:REVISIONUSER}}' 337 ); 338 $this->assertStringNotContainsString( '~~~', $pstText, 'signature ~~~' ); 339 $this->assertStringContainsString( '(' . $sysopName . ')', $pstText, '{{subst:REVISIONUSER}}' ); 340 $this->assertStringContainsString( ':' . $sysopName . '|', $pstText, 'signature ~~~' ); 341 342 $this->assertFalse( $updater2->isCreation() ); 343 $this->assertTrue( $updater2->isChange() ); 344 } 345 346 // TODO: test failure of prepareContent() when called again... 347 // - with different user 348 // - with different update 349 // - after calling prepareUpdate() 350 351 /** 352 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate() 353 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isUpdatePrepared() 354 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation() 355 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots() 356 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot() 357 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent() 358 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles() 359 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles() 360 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput() 361 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput() 362 */ 363 public function testPrepareUpdate() { 364 $page = $this->getPage( __METHOD__ ); 365 366 $mainContent1 = new WikitextContent( 'first [[main]] ~~~' ); 367 $rev1 = $this->createRevision( $page, 'first', $mainContent1 ); 368 $updater1 = $this->getDerivedPageDataUpdater( $page, $rev1 ); 369 370 $options = []; // TODO: test *all* the options... 371 $updater1->prepareUpdate( $rev1, $options ); 372 373 $this->assertTrue( $updater1->isUpdatePrepared() ); 374 $this->assertTrue( $updater1->isContentPrepared() ); 375 $this->assertTrue( $updater1->isCreation() ); 376 $this->assertTrue( $updater1->isChange() ); 377 $this->assertFalse( $updater1->isContentDeleted() ); 378 379 $this->assertNotNull( $updater1->getRevision() ); 380 $this->assertNotNull( $updater1->getRenderedRevision() ); 381 382 $this->assertEquals( [ 'main' ], $updater1->getSlots()->getSlotRoles() ); 383 $this->assertEquals( [ 'main' ], array_keys( $updater1->getSlots()->getOriginalSlots() ) ); 384 $this->assertEquals( [], array_keys( $updater1->getSlots()->getInheritedSlots() ) ); 385 $this->assertEquals( [ 'main' ], $updater1->getModifiedSlotRoles() ); 386 $this->assertEquals( [ 'main' ], $updater1->getTouchedSlotRoles() ); 387 388 // TODO: MCR: test multiple slots, test slot removal! 389 390 $this->assertInstanceOf( SlotRecord::class, $updater1->getRawSlot( SlotRecord::MAIN ) ); 391 $this->assertStringNotContainsString( 392 '~~~~', 393 $updater1->getRawContent( SlotRecord::MAIN )->serialize() 394 ); 395 396 $mainOutput = $updater1->getCanonicalParserOutput(); 397 $this->assertStringContainsString( 'first', $mainOutput->getText() ); 398 $this->assertStringContainsString( '<a ', $mainOutput->getText() ); 399 $this->assertNotEmpty( $mainOutput->getLinks() ); 400 401 $canonicalOutput = $updater1->getCanonicalParserOutput(); 402 $this->assertStringContainsString( 'first', $canonicalOutput->getText() ); 403 $this->assertStringContainsString( '<a ', $canonicalOutput->getText() ); 404 $this->assertNotEmpty( $canonicalOutput->getLinks() ); 405 406 $mainContent2 = new WikitextContent( 'second' ); 407 $rev2 = $this->createRevision( $page, 'second', $mainContent2 ); 408 $updater2 = $this->getDerivedPageDataUpdater( $page, $rev2 ); 409 410 $options = []; // TODO: test *all* the options... 411 $updater2->prepareUpdate( $rev2, $options ); 412 413 $this->assertFalse( $updater2->isCreation() ); 414 $this->assertTrue( $updater2->isChange() ); 415 416 $canonicalOutput = $updater2->getCanonicalParserOutput(); 417 $this->assertStringContainsString( 'second', $canonicalOutput->getText() ); 418 } 419 420 /** 421 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate() 422 */ 423 public function testPrepareUpdateReusesParserOutput() { 424 $user = $this->getTestUser()->getUser(); 425 $page = $this->getPage( __METHOD__ ); 426 427 $mainContent1 = new WikitextContent( 'first [[main]] ~~~' ); 428 429 $update = new RevisionSlotsUpdate(); 430 $update->modifyContent( SlotRecord::MAIN, $mainContent1 ); 431 $updater = $this->getDerivedPageDataUpdater( $page ); 432 $updater->prepareContent( $user, $update, false ); 433 434 $mainOutput = $updater->getSlotParserOutput( SlotRecord::MAIN ); 435 $canonicalOutput = $updater->getCanonicalParserOutput(); 436 437 $rev = $this->createRevision( $page, 'first', $mainContent1 ); 438 439 $options = []; // TODO: test *all* the options... 440 $updater->prepareUpdate( $rev, $options ); 441 442 $this->assertTrue( $updater->isUpdatePrepared() ); 443 $this->assertTrue( $updater->isContentPrepared() ); 444 445 $this->assertSame( $mainOutput, $updater->getSlotParserOutput( SlotRecord::MAIN ) ); 446 $this->assertSame( $canonicalOutput, $updater->getCanonicalParserOutput() ); 447 } 448 449 /** 450 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate() 451 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput() 452 */ 453 public function testPrepareUpdateOutputReset() { 454 $user = $this->getTestUser()->getUser(); 455 $page = $this->getPage( __METHOD__ ); 456 457 $mainContent1 = new WikitextContent( 'first --{{REVISIONID}}--' ); 458 459 $update = new RevisionSlotsUpdate(); 460 $update->modifyContent( SlotRecord::MAIN, $mainContent1 ); 461 $updater = $this->getDerivedPageDataUpdater( $page ); 462 $updater->prepareContent( $user, $update, false ); 463 464 $mainOutput = $updater->getSlotParserOutput( SlotRecord::MAIN ); 465 $canonicalOutput = $updater->getCanonicalParserOutput(); 466 467 // prevent optimization on matching speculative ID 468 $mainOutput->setSpeculativeRevIdUsed( 0 ); 469 $canonicalOutput->setSpeculativeRevIdUsed( 0 ); 470 471 $rev = $this->createRevision( $page, 'first', $mainContent1 ); 472 473 $options = []; // TODO: test *all* the options... 474 $updater->prepareUpdate( $rev, $options ); 475 476 $this->assertTrue( $updater->isUpdatePrepared() ); 477 $this->assertTrue( $updater->isContentPrepared() ); 478 479 // ParserOutput objects should have been flushed. 480 $this->assertNotSame( $mainOutput, $updater->getSlotParserOutput( SlotRecord::MAIN ) ); 481 $this->assertNotSame( $canonicalOutput, $updater->getCanonicalParserOutput() ); 482 483 $html = $updater->getCanonicalParserOutput()->getText(); 484 $this->assertStringContainsString( '--' . $rev->getId() . '--', $html ); 485 486 // TODO: MCR: ensure that when the main slot uses {{REVISIONID}} but another slot is 487 // updated, the main slot is still re-rendered! 488 } 489 490 // TODO: test failure of prepareUpdate() when called again with a different revision 491 // TODO: test failure of prepareUpdate() on inconsistency with prepareContent. 492 493 /** 494 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit() 495 */ 496 public function testGetPreparedEditAfterPrepareContent() { 497 $user = $this->getTestUser()->getUser(); 498 499 $mainContent = new WikitextContent( 'first [[main]] ~~~' ); 500 $update = new RevisionSlotsUpdate(); 501 $update->modifyContent( SlotRecord::MAIN, $mainContent ); 502 503 $updater = $this->getDerivedPageDataUpdater( __METHOD__ ); 504 $updater->prepareContent( $user, $update, false ); 505 506 $canonicalOutput = $updater->getCanonicalParserOutput(); 507 508 $preparedEdit = $updater->getPreparedEdit(); 509 $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp ); 510 $this->assertSame( $canonicalOutput, $preparedEdit->output ); 511 $this->assertSame( $mainContent, $preparedEdit->newContent ); 512 $this->assertSame( $updater->getRawContent( SlotRecord::MAIN ), $preparedEdit->pstContent ); 513 $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts ); 514 $this->assertSame( null, $preparedEdit->revid ); 515 } 516 517 /** 518 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit() 519 */ 520 public function testGetPreparedEditAfterPrepareUpdate() { 521 $clock = MWTimestamp::convert( TS_UNIX, '20100101000000' ); 522 MWTimestamp::setFakeTime( static function () use ( &$clock ) { 523 return $clock++; 524 } ); 525 526 $page = $this->getPage( __METHOD__ ); 527 528 $mainContent = new WikitextContent( 'first [[main]] ~~~' ); 529 $update = new MutableRevisionSlots(); 530 $update->setContent( SlotRecord::MAIN, $mainContent ); 531 532 $rev = $this->createRevision( $page, __METHOD__ ); 533 534 $updater = $this->getDerivedPageDataUpdater( $page ); 535 $updater->prepareUpdate( $rev ); 536 537 $canonicalOutput = $updater->getCanonicalParserOutput(); 538 539 $preparedEdit = $updater->getPreparedEdit(); 540 $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp ); 541 $this->assertSame( $canonicalOutput, $preparedEdit->output ); 542 $this->assertSame( $updater->getRawContent( SlotRecord::MAIN ), $preparedEdit->pstContent ); 543 $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts ); 544 $this->assertSame( $rev->getId(), $preparedEdit->revid ); 545 } 546 547 public function testGetSecondaryDataUpdatesAfterPrepareContent() { 548 $user = $this->getTestUser()->getUser(); 549 $page = $this->getPage( __METHOD__ ); 550 $this->createRevision( $page, __METHOD__ ); 551 552 $mainContent1 = new WikitextContent( 'first' ); 553 554 $update = new RevisionSlotsUpdate(); 555 $update->modifyContent( SlotRecord::MAIN, $mainContent1 ); 556 $updater = $this->getDerivedPageDataUpdater( $page ); 557 $updater->prepareContent( $user, $update, false ); 558 559 $dataUpdates = $updater->getSecondaryDataUpdates(); 560 561 $this->assertNotEmpty( $dataUpdates ); 562 563 $linksUpdates = array_filter( $dataUpdates, static function ( $du ) { 564 return $du instanceof LinksUpdate; 565 } ); 566 $this->assertCount( 1, $linksUpdates ); 567 } 568 569 public function testAvoidSecondaryDataUpdatesOnNonHTMLContentHandlers() { 570 $this->setMwGlobals( [ 571 'wgContentHandlers' => [ 572 CONTENT_MODEL_WIKITEXT => WikitextContentHandler::class, 573 'testing' => DummyContentHandlerForTesting::class, 574 ], 575 ] ); 576 577 MediaWikiServices::getInstance()->resetServiceForTesting( 'ContentHandlerFactory' ); 578 $user = $this->getTestUser()->getUser(); 579 $page = $this->getPage( __METHOD__ ); 580 $this->createRevision( $page, __METHOD__ ); 581 582 $contentHandler = new DummyContentHandlerForTesting( 'testing' ); 583 $mainContent1 = $contentHandler->unserializeContent( serialize( 'first' ) ); 584 $update = new RevisionSlotsUpdate(); 585 $pcache = MediaWikiServices::getInstance()->getParserCache(); 586 $pcache->deleteOptionsKey( $page ); 587 $rev = $this->createRevision( $page, 'first', $mainContent1 ); 588 589 // Run updates 590 $update->modifyContent( SlotRecord::MAIN, $mainContent1 ); 591 $updater = $this->getDerivedPageDataUpdater( $page ); 592 $updater->prepareContent( $user, $update, false ); 593 $dataUpdates = $updater->getSecondaryDataUpdates(); 594 $updater->prepareUpdate( $rev ); 595 $updater->doUpdates(); 596 597 // Links updates should be triggered 598 $this->assertNotEmpty( $dataUpdates ); 599 $linksUpdates = array_filter( $dataUpdates, static function ( $du ) { 600 return $du instanceof LinksUpdate; 601 } ); 602 $this->assertCount( 1, $linksUpdates ); 603 604 // Parser cache should not be populated. 605 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() ); 606 $this->assertFalse( $cached ); 607 } 608 609 public function testGetSecondaryDataUpdatesDeleted() { 610 $user = $this->getTestUser()->getUser(); 611 $page = $this->getPage( __METHOD__ ); 612 $this->createRevision( $page, __METHOD__ ); 613 614 $mainContent1 = new WikitextContent( 'first' ); 615 616 $update = new RevisionSlotsUpdate(); 617 $update->modifyContent( SlotRecord::MAIN, $mainContent1 ); 618 $updater = $this->getDerivedPageDataUpdater( $page ); 619 $updater->prepareContent( $user, $update, false ); 620 621 // Test that nothing happens if the page was deleted in the meantime 622 // This can happen when started by the job queue 623 $page->doDeleteArticleReal( 'Test', $user ); 624 625 $dataUpdates = $updater->getSecondaryDataUpdates(); 626 627 $this->assertEmpty( $dataUpdates ); 628 } 629 630 /** 631 * @param string $name 632 * 633 * @return ContentHandler 634 */ 635 private function defineMockContentModelForUpdateTesting( $name ) { 636 /** @var ContentHandler|MockObject $handler */ 637 $handler = $this->getMockBuilder( TextContentHandler::class ) 638 ->setConstructorArgs( [ $name ] ) 639 ->onlyMethods( 640 [ 'getSecondaryDataUpdates', 'getDeletionUpdates', 'unserializeContent' ] 641 ) 642 ->getMock(); 643 644 $dataUpdate = new MWCallableUpdate( 'time' ); 645 $dataUpdate->_name = "$name data update"; 646 647 $deletionUpdate = new MWCallableUpdate( 'time' ); 648 $deletionUpdate->_name = "$name deletion update"; 649 650 $handler->method( 'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] ); 651 $handler->method( 'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] ); 652 $handler->method( 'unserializeContent' )->willReturnCallback( 653 function ( $text ) use ( $handler ) { 654 return $this->createMockContent( $handler, $text ); 655 } 656 ); 657 658 $this->mergeMwGlobalArrayValue( 659 'wgContentHandlers', [ 660 $name => static function () use ( $handler ){ 661 return $handler; 662 } 663 ] 664 ); 665 666 return $handler; 667 } 668 669 /** 670 * @param ContentHandler $handler 671 * @param string $text 672 * 673 * @return Content 674 */ 675 private function createMockContent( ContentHandler $handler, $text ) { 676 /** @var Content|MockObject $content */ 677 $content = $this->getMockBuilder( TextContent::class ) 678 ->setConstructorArgs( [ $text ] ) 679 ->onlyMethods( [ 'getModel', 'getContentHandler' ] ) 680 ->getMock(); 681 682 $content->method( 'getModel' )->willReturn( $handler->getModelID() ); 683 $content->method( 'getContentHandler' )->willReturn( $handler ); 684 685 return $content; 686 } 687 688 public function testGetSecondaryDataUpdatesWithSlotRemoval() { 689 $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' ); 690 $a1 = $this->defineMockContentModelForUpdateTesting( 'A1' ); 691 $m2 = $this->defineMockContentModelForUpdateTesting( 'M2' ); 692 693 $role = 'dpdu-test-a1'; 694 $slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry(); 695 $slotRoleRegistry->defineRoleWithModel( 696 $role, 697 $a1->getModelID() 698 ); 699 700 // pin the service instance for this test 701 $this->setService( 'SlotRoleRegistry', $slotRoleRegistry ); 702 703 $mainContent1 = $this->createMockContent( $m1, 'main 1' ); 704 $auxContent1 = $this->createMockContent( $a1, 'aux 1' ); 705 $mainContent2 = $this->createMockContent( $m2, 'main 2' ); 706 707 $user = $this->getTestUser()->getUser(); 708 $page = $this->getPage( __METHOD__ ); 709 $this->createRevision( 710 $page, 711 __METHOD__, 712 [ 'main' => $mainContent1, $role => $auxContent1 ] 713 ); 714 715 $update = new RevisionSlotsUpdate(); 716 $update->modifyContent( SlotRecord::MAIN, $mainContent2 ); 717 $update->removeSlot( $role ); 718 719 $page = $this->getPage( __METHOD__ ); 720 $updater = $this->getDerivedPageDataUpdater( $page ); 721 $updater->prepareContent( $user, $update, false ); 722 723 $dataUpdates = $updater->getSecondaryDataUpdates(); 724 725 $this->assertNotEmpty( $dataUpdates ); 726 727 $updateNames = array_map( static function ( $du ) { 728 return $du->_name ?? get_class( $du ); 729 }, $dataUpdates ); 730 731 $this->assertContains( LinksUpdate::class, $updateNames ); 732 $this->assertContains( 'A1 deletion update', $updateNames ); 733 $this->assertContains( 'M2 data update', $updateNames ); 734 $this->assertNotContains( 'M1 data update', $updateNames ); 735 } 736 737 /** 738 * Creates a dummy MutableRevisionRecord without touching the database. 739 * 740 * @param Title $title 741 * @param RevisionSlotsUpdate $update 742 * @param User $user 743 * @param string $comment 744 * @param int $id 745 * @param int $parentId 746 * 747 * @return MutableRevisionRecord 748 */ 749 private function makeRevision( 750 Title $title, 751 RevisionSlotsUpdate $update, 752 User $user, 753 $comment, 754 $id = 0, 755 $parentId = 0 756 ) { 757 $rev = new MutableRevisionRecord( $title ); 758 759 $rev->applyUpdate( $update ); 760 $rev->setUser( $user ); 761 $rev->setComment( CommentStoreComment::newUnsavedComment( $comment ) ); 762 $rev->setPageId( $title->getArticleID() ); 763 $rev->setParentId( $parentId ); 764 765 if ( $id ) { 766 $rev->setId( $id ); 767 } 768 769 return $rev; 770 } 771 772 public function provideIsReusableFor() { 773 $title = $this->makeMockTitle( __CLASS__, [ 'id' => 23 ] ); 774 775 $user1 = User::newFromName( 'Alice' ); 776 $user2 = User::newFromName( 'Bob' ); 777 778 $content1 = new WikitextContent( 'one' ); 779 $content2 = new WikitextContent( 'two' ); 780 781 $update1 = new RevisionSlotsUpdate(); 782 $update1->modifyContent( SlotRecord::MAIN, $content1 ); 783 784 $update1b = new RevisionSlotsUpdate(); 785 $update1b->modifyContent( 'xyz', $content1 ); 786 787 $update2 = new RevisionSlotsUpdate(); 788 $update2->modifyContent( SlotRecord::MAIN, $content2 ); 789 790 $rev1 = $this->makeRevision( $title, $update1, $user1, 'rev1', 11 ); 791 $rev1b = $this->makeRevision( $title, $update1b, $user1, 'rev1', 11 ); 792 793 $rev2 = $this->makeRevision( $title, $update2, $user1, 'rev2', 12 ); 794 $rev2x = $this->makeRevision( $title, $update2, $user2, 'rev2', 12 ); 795 $rev2y = $this->makeRevision( $title, $update2, $user1, 'rev2', 122 ); 796 797 yield 'any' => [ 798 '$prepUser' => null, 799 '$prepRevision' => null, 800 '$prepUpdate' => null, 801 '$forUser' => null, 802 '$forRevision' => null, 803 '$forUpdate' => null, 804 '$forParent' => null, 805 '$isReusable' => true, 806 ]; 807 yield 'for any' => [ 808 '$prepUser' => $user1, 809 '$prepRevision' => $rev1, 810 '$prepUpdate' => $update1, 811 '$forUser' => null, 812 '$forRevision' => null, 813 '$forUpdate' => null, 814 '$forParent' => null, 815 '$isReusable' => true, 816 ]; 817 yield 'unprepared' => [ 818 '$prepUser' => null, 819 '$prepRevision' => null, 820 '$prepUpdate' => null, 821 '$forUser' => $user1, 822 '$forRevision' => $rev1, 823 '$forUpdate' => $update1, 824 '$forParent' => 0, 825 '$isReusable' => true, 826 ]; 827 yield 'match prepareContent' => [ 828 '$prepUser' => $user1, 829 '$prepRevision' => null, 830 '$prepUpdate' => $update1, 831 '$forUser' => $user1, 832 '$forRevision' => null, 833 '$forUpdate' => $update1, 834 '$forParent' => 0, 835 '$isReusable' => true, 836 ]; 837 yield 'match prepareUpdate' => [ 838 '$prepUser' => null, 839 '$prepRevision' => $rev1, 840 '$prepUpdate' => null, 841 '$forUser' => $user1, 842 '$forRevision' => $rev1, 843 '$forUpdate' => null, 844 '$forParent' => 0, 845 '$isReusable' => true, 846 ]; 847 yield 'match all' => [ 848 '$prepUser' => $user1, 849 '$prepRevision' => $rev1, 850 '$prepUpdate' => $update1, 851 '$forUser' => $user1, 852 '$forRevision' => $rev1, 853 '$forUpdate' => $update1, 854 '$forParent' => 0, 855 '$isReusable' => true, 856 ]; 857 yield 'mismatch prepareContent update' => [ 858 '$prepUser' => $user1, 859 '$prepRevision' => null, 860 '$prepUpdate' => $update1, 861 '$forUser' => $user1, 862 '$forRevision' => null, 863 '$forUpdate' => $update1b, 864 '$forParent' => 0, 865 '$isReusable' => false, 866 ]; 867 yield 'mismatch prepareContent user' => [ 868 '$prepUser' => $user1, 869 '$prepRevision' => null, 870 '$prepUpdate' => $update1, 871 '$forUser' => $user2, 872 '$forRevision' => null, 873 '$forUpdate' => $update1, 874 '$forParent' => 0, 875 '$isReusable' => false, 876 ]; 877 yield 'mismatch prepareContent parent' => [ 878 '$prepUser' => $user1, 879 '$prepRevision' => null, 880 '$prepUpdate' => $update1, 881 '$forUser' => $user1, 882 '$forRevision' => null, 883 '$forUpdate' => $update1, 884 '$forParent' => 7, 885 '$isReusable' => false, 886 ]; 887 yield 'mismatch prepareUpdate revision update' => [ 888 '$prepUser' => null, 889 '$prepRevision' => $rev1, 890 '$prepUpdate' => null, 891 '$forUser' => null, 892 '$forRevision' => $rev1b, 893 '$forUpdate' => null, 894 '$forParent' => 0, 895 '$isReusable' => false, 896 ]; 897 yield 'mismatch prepareUpdate revision id' => [ 898 '$prepUser' => null, 899 '$prepRevision' => $rev2, 900 '$prepUpdate' => null, 901 '$forUser' => null, 902 '$forRevision' => $rev2y, 903 '$forUpdate' => null, 904 '$forParent' => 0, 905 '$isReusable' => false, 906 ]; 907 } 908 909 /** 910 * @dataProvider provideIsReusableFor 911 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isReusableFor() 912 */ 913 public function testIsReusableFor( 914 ?User $prepUser, 915 ?RevisionRecord $prepRevision, 916 ?RevisionSlotsUpdate $prepUpdate, 917 ?User $forUser, 918 ?RevisionRecord $forRevision, 919 ?RevisionSlotsUpdate $forUpdate, 920 $forParent, 921 $isReusable 922 ) { 923 $updater = $this->getDerivedPageDataUpdater( __METHOD__ ); 924 925 if ( $prepUpdate ) { 926 $updater->prepareContent( $prepUser, $prepUpdate, false ); 927 } 928 929 if ( $prepRevision ) { 930 $updater->prepareUpdate( $prepRevision ); 931 } 932 933 $this->assertSame( 934 $isReusable, 935 $updater->isReusableFor( $forUser, $forRevision, $forUpdate, $forParent ) 936 ); 937 } 938 939 /** 940 * * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCountable 941 */ 942 public function testIsCountableNotContentPage() { 943 $updater = $this->getDerivedPageDataUpdater( 944 Title::newFromText( 'Main_Page', NS_TALK ) 945 ); 946 self::assertFalse( $updater->isCountable() ); 947 } 948 949 public function provideIsCountable() { 950 yield 'deleted revision' => [ 951 '$articleCountMethod' => 'any', 952 '$wikitextContent' => 'Test', 953 '$revisionVisibility' => RevisionRecord::SUPPRESSED_ALL, 954 '$isCountable' => false 955 ]; 956 yield 'redirect' => [ 957 '$articleCountMethod' => 'any', 958 '$wikitextContent' => '#REDIRECT [[Main_Page]]', 959 '$revisionVisibility' => 0, 960 '$isCountable' => false 961 ]; 962 yield 'no links count method any' => [ 963 '$articleCountMethod' => 'any', 964 '$wikitextContent' => 'Test', 965 '$revisionVisibility' => 0, 966 '$isCountable' => true 967 ]; 968 yield 'no links count method link' => [ 969 '$articleCountMethod' => 'link', 970 '$wikitextContent' => 'Test', 971 '$revisionVisibility' => 0, 972 '$isCountable' => false 973 ]; 974 yield 'with links count method link' => [ 975 '$articleCountMethod' => 'link', 976 '$wikitextContent' => '[[Test]]', 977 '$revisionVisibility' => 0, 978 '$isCountable' => true 979 ]; 980 } 981 982 /** 983 * @dataProvider provideIsCountable 984 * 985 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCountable 986 */ 987 public function testIsCountable( 988 $articleCountMethod, 989 $wikitextContent, 990 $revisionVisibility, 991 $isCountable 992 ) { 993 $this->setMwGlobals( [ 'wgArticleCountMethod' => $articleCountMethod ] ); 994 $title = $this->getTitle( 'Main_Page' ); 995 $content = new WikitextContent( $wikitextContent ); 996 $update = new RevisionSlotsUpdate(); 997 $update->modifyContent( SlotRecord::MAIN, $content ); 998 $revision = $this->makeRevision( $title, $update, User::newFromName( 'Alice' ), 'rev1', 13 ); 999 $revision->setVisibility( $revisionVisibility ); 1000 $updater = $this->getDerivedPageDataUpdater( $title ); 1001 $updater->prepareUpdate( $revision ); 1002 self::assertSame( $isCountable, $updater->isCountable() ); 1003 } 1004 1005 /** 1006 * @throws \MWException 1007 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCountable 1008 */ 1009 public function testIsCountableNoModifiedSlots() { 1010 $page = $this->getPage( __METHOD__ ); 1011 $content = [ 'main' => new WikitextContent( '[[Test]]' ) ]; 1012 $rev = $this->createRevision( $page, 'first', $content ); 1013 $nullRevision = MutableRevisionRecord::newFromParentRevision( $rev ); 1014 $nullRevision->setId( 14 ); 1015 $updater = $this->getDerivedPageDataUpdater( $page, $nullRevision ); 1016 $updater->prepareUpdate( $nullRevision ); 1017 $this->assertTrue( $updater->isCountable() ); 1018 } 1019 1020 /** 1021 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates() 1022 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates() 1023 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate() 1024 */ 1025 public function testDoUpdates() { 1026 $page = $this->getPage( __METHOD__ ); 1027 1028 $content = [ 'main' => new WikitextContent( 'first [[main]]' ) ]; 1029 1030 $content['aux'] = new WikitextContent( 'Aux [[Nix]]' ); 1031 1032 $slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry(); 1033 if ( !$slotRoleRegistry->isDefinedRole( 'aux' ) ) { 1034 $slotRoleRegistry->defineRoleWithModel( 1035 'aux', 1036 CONTENT_MODEL_WIKITEXT 1037 ); 1038 } 1039 1040 $rev = $this->createRevision( $page, 'first', $content ); 1041 $pageId = $page->getId(); 1042 1043 $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' ); 1044 $this->db->delete( 'pagelinks', '*' ); 1045 1046 $pcache = MediaWikiServices::getInstance()->getParserCache(); 1047 $pcache->deleteOptionsKey( $page ); 1048 1049 $updater = $this->getDerivedPageDataUpdater( $page, $rev ); 1050 $updater->setArticleCountMethod( 'link' ); 1051 1052 $options = []; // TODO: test *all* the options... 1053 $updater->prepareUpdate( $rev, $options ); 1054 1055 $updater->doUpdates(); 1056 1057 // links table update 1058 $pageLinks = $this->db->select( 1059 'pagelinks', 1060 '*', 1061 [ 'pl_from' => $pageId ], 1062 __METHOD__, 1063 [ 'ORDER BY' => [ 'pl_namespace', 'pl_title' ] ] 1064 ); 1065 1066 $pageLinksRow = $pageLinks->fetchObject(); 1067 $this->assertIsObject( $pageLinksRow ); 1068 $this->assertSame( 'Main', $pageLinksRow->pl_title ); 1069 1070 $pageLinksRow = $pageLinks->fetchObject(); 1071 $this->assertIsObject( $pageLinksRow ); 1072 $this->assertSame( 'Nix', $pageLinksRow->pl_title ); 1073 1074 // parser cache update 1075 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() ); 1076 $this->assertIsObject( $cached ); 1077 $this->assertEquals( $updater->getCanonicalParserOutput(), $cached ); 1078 1079 // site stats 1080 $stats = $this->db->selectRow( 'site_stats', '*', '1=1' ); 1081 $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages ); 1082 $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits ); 1083 $this->assertSame( $oldStats->ss_good_articles + 1, (int)$stats->ss_good_articles ); 1084 1085 // TODO: MCR: test data updates for additional slots! 1086 // TODO: test update for edit without page creation 1087 // TODO: test message cache purge 1088 // TODO: test module cache purge 1089 // TODO: test CDN purge 1090 // TODO: test newtalk update 1091 // TODO: test search update 1092 // TODO: test site stats good_articles while turning the page into (or back from) a redir. 1093 // TODO: test category membership update (with setRcWatchCategoryMembership()) 1094 } 1095 1096 /** 1097 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates() 1098 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates() 1099 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate() 1100 */ 1101 public function testDoUpdatesCacheSaveDeferral_canonical() { 1102 $page = $this->getPage( __METHOD__ ); 1103 1104 // Case where user has canonical parser options 1105 $content = [ 'main' => new WikitextContent( 'rev ID ver #1: {{REVISIONID}}' ) ]; 1106 $rev = $this->createRevision( $page, 'first', $content ); 1107 $pcache = MediaWikiServices::getInstance()->getParserCache(); 1108 $pcache->deleteOptionsKey( $page ); 1109 1110 $this->db->startAtomic( __METHOD__ ); // let deferred updates queue up 1111 1112 $updater = $this->getDerivedPageDataUpdater( $page, $rev ); 1113 $updater->prepareUpdate( $rev, [] ); 1114 $updater->doUpdates(); 1115 1116 $this->assertGreaterThan( 0, DeferredUpdates::pendingUpdatesCount(), 'Pending updates' ); 1117 $this->assertNotFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) ); 1118 1119 $this->db->endAtomic( __METHOD__ ); // run deferred updates 1120 1121 $this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' ); 1122 } 1123 1124 /** 1125 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates() 1126 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates() 1127 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate() 1128 */ 1129 public function testDoUpdatesCacheSaveDeferral_noncanonical() { 1130 $page = $this->getPage( __METHOD__ ); 1131 1132 // Case where user does not have canonical parser options 1133 $user = $this->getMutableTestUser()->getUser(); 1134 $user->setOption( 1135 'thumbsize', 1136 $user->getOption( 'thumbsize' ) + 1 1137 ); 1138 $content = [ 'main' => new WikitextContent( 'rev ID ver #2: {{REVISIONID}}' ) ]; 1139 $rev = $this->createRevision( $page, 'first', $content, $user ); 1140 $pcache = MediaWikiServices::getInstance()->getParserCache(); 1141 $pcache->deleteOptionsKey( $page ); 1142 1143 $this->db->startAtomic( __METHOD__ ); // let deferred updates queue up 1144 1145 $updater = $this->getDerivedPageDataUpdater( $page, $rev, $user ); 1146 $updater->prepareUpdate( $rev, [] ); 1147 $updater->doUpdates(); 1148 1149 $this->assertGreaterThan( 1, DeferredUpdates::pendingUpdatesCount(), 'Pending updates' ); 1150 $this->assertFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) ); 1151 1152 $this->db->endAtomic( __METHOD__ ); // run deferred updates 1153 1154 $this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' ); 1155 $this->assertNotFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) ); 1156 } 1157 1158 public function provideEnqueueRevertedTagUpdateJob() { 1159 return [ 1160 'approved' => [ true, 1 ], 1161 'not approved' => [ false, 0 ] 1162 ]; 1163 } 1164 1165 /** 1166 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates 1167 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::maybeEnqueueRevertedTagUpdateJob 1168 * @dataProvider provideEnqueueRevertedTagUpdateJob 1169 */ 1170 public function testEnqueueRevertedTagUpdateJob( bool $approved, int $queueSize ) { 1171 $page = $this->getPage( __METHOD__ ); 1172 1173 $content = [ 'main' => new WikitextContent( '1' ) ]; 1174 $rev = $this->createRevision( $page, '', $content ); 1175 $editResult = new EditResult( 1176 false, 1177 10, 1178 EditResult::REVERT_ROLLBACK, 1179 11, 1180 12, 1181 true, 1182 false, 1183 [ 'mw-rollback' ] 1184 ); 1185 1186 $updater = $this->getDerivedPageDataUpdater( $page, $rev ); 1187 1188 $updater->prepareUpdate( $rev, [ 1189 'editResult' => $editResult, 1190 'approved' => $approved 1191 ] ); 1192 $updater->doUpdates(); 1193 1194 $services = MediaWikiServices::getInstance(); 1195 $editResultCache = new EditResultCache( 1196 $services->getMainObjectStash(), 1197 $services->getDBLoadBalancer(), 1198 new ServiceOptions( 1199 EditResultCache::CONSTRUCTOR_OPTIONS, 1200 [ 'RCMaxAge' => BagOStuff::TTL_MONTH ] 1201 ) 1202 ); 1203 1204 if ( $approved ) { 1205 $this->assertNull( 1206 $editResultCache->get( $rev->getId() ), 1207 'EditResult should not be cached when the revert is approved' 1208 ); 1209 } else { 1210 $this->assertEquals( 1211 $editResult, 1212 $editResultCache->get( $rev->getId() ), 1213 'EditResult should be cached when the revert is not approved' 1214 ); 1215 } 1216 1217 $jobQueueGroup = JobQueueGroup::singleton(); 1218 $jobQueue = $jobQueueGroup->get( 'revertedTagUpdate' ); 1219 $this->assertSame( 1220 $queueSize, 1221 $jobQueue->getSize() 1222 ); 1223 } 1224 1225 /** 1226 * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate() 1227 */ 1228 public function testDoParserCacheUpdate() { 1229 $slotRoleRegistry = MediaWikiServices::getInstance()->getSlotRoleRegistry(); 1230 if ( !$slotRoleRegistry->isDefinedRole( 'aux' ) ) { 1231 $slotRoleRegistry->defineRoleWithModel( 1232 'aux', 1233 CONTENT_MODEL_WIKITEXT 1234 ); 1235 } 1236 1237 $page = $this->getPage( __METHOD__ ); 1238 $this->createRevision( $page, 'Dummy' ); 1239 1240 $user = $this->getTestUser()->getUser(); 1241 1242 $update = new RevisionSlotsUpdate(); 1243 $update->modifyContent( 'main', new WikitextContent( 'first [[Main]]' ) ); 1244 $update->modifyContent( 'aux', new WikitextContent( 'Aux [[Nix]]' ) ); 1245 1246 // Emulate update after edit ---------- 1247 $pcache = MediaWikiServices::getInstance()->getParserCache(); 1248 $pcache->deleteOptionsKey( $page ); 1249 1250 $rev = $this->makeRevision( $page->getTitle(), $update, $user, 'rev', null ); 1251 $rev->setTimestamp( '20100101000000' ); 1252 $rev->setParentId( $page->getLatest() ); 1253 1254 $updater = $this->getDerivedPageDataUpdater( $page ); 1255 $updater->prepareContent( $user, $update, false ); 1256 1257 $rev->setId( 11 ); 1258 $updater->prepareUpdate( $rev ); 1259 1260 // Force the page timestamp, so we notice whether ParserOutput::getTimestamp 1261 // or ParserOutput::getCacheTime are used. 1262 $page->setTimestamp( $rev->getTimestamp() ); 1263 $updater->doParserCacheUpdate(); 1264 1265 // The cached ParserOutput should not use the revision timestamp 1266 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions(), true ); 1267 $this->assertIsObject( $cached ); 1268 $this->assertEquals( $updater->getCanonicalParserOutput(), $cached ); 1269 1270 $this->assertSame( $rev->getTimestamp(), $cached->getCacheTime() ); 1271 $this->assertSame( $rev->getId(), $cached->getCacheRevisionId() ); 1272 1273 // Emulate forced update of an old revision ---------- 1274 $pcache->deleteOptionsKey( $page ); 1275 1276 $updater = $this->getDerivedPageDataUpdater( $page ); 1277 $updater->prepareUpdate( $rev ); 1278 1279 // Force the page timestamp, so we notice whether ParserOutput::getTimestamp 1280 // or ParserOutput::getCacheTime are used. 1281 $page->setTimestamp( $rev->getTimestamp() ); 1282 $updater->doParserCacheUpdate(); 1283 1284 // The cached ParserOutput should not use the revision timestamp 1285 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions(), true ); 1286 $this->assertIsObject( $cached ); 1287 $this->assertEquals( $updater->getCanonicalParserOutput(), $cached ); 1288 1289 $this->assertGreaterThan( $rev->getTimestamp(), $cached->getCacheTime() ); 1290 $this->assertSame( $rev->getId(), $cached->getCacheRevisionId() ); 1291 } 1292 1293} 1294