DerivedPageDataUpdaterTest.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935
  1. <?php
  2. namespace MediaWiki\Tests\Storage;
  3. use CommentStoreComment;
  4. use Content;
  5. use ContentHandler;
  6. use LinksUpdate;
  7. use MediaWiki\MediaWikiServices;
  8. use MediaWiki\Storage\DerivedPageDataUpdater;
  9. use MediaWiki\Storage\MutableRevisionRecord;
  10. use MediaWiki\Storage\MutableRevisionSlots;
  11. use MediaWiki\Storage\RevisionRecord;
  12. use MediaWiki\Storage\RevisionSlotsUpdate;
  13. use MediaWiki\Storage\SlotRecord;
  14. use MediaWikiTestCase;
  15. use MWCallableUpdate;
  16. use PHPUnit\Framework\MockObject\MockObject;
  17. use TextContent;
  18. use TextContentHandler;
  19. use Title;
  20. use User;
  21. use Wikimedia\TestingAccessWrapper;
  22. use WikiPage;
  23. use WikitextContent;
  24. /**
  25. * @group Database
  26. *
  27. * @covers \MediaWiki\Storage\DerivedPageDataUpdater
  28. */
  29. class DerivedPageDataUpdaterTest extends MediaWikiTestCase {
  30. /**
  31. * @param string $title
  32. *
  33. * @return Title
  34. */
  35. private function getTitle( $title ) {
  36. return Title::makeTitleSafe( $this->getDefaultWikitextNS(), $title );
  37. }
  38. /**
  39. * @param string|Title $title
  40. *
  41. * @return WikiPage
  42. */
  43. private function getPage( $title ) {
  44. $title = ( $title instanceof Title ) ? $title : $this->getTitle( $title );
  45. return WikiPage::factory( $title );
  46. }
  47. /**
  48. * @param string|Title|WikiPage $page
  49. *
  50. * @return DerivedPageDataUpdater
  51. */
  52. private function getDerivedPageDataUpdater( $page, RevisionRecord $rec = null ) {
  53. if ( is_string( $page ) || $page instanceof Title ) {
  54. $page = $this->getPage( $page );
  55. }
  56. $page = TestingAccessWrapper::newFromObject( $page );
  57. return $page->getDerivedDataUpdater( null, $rec );
  58. }
  59. /**
  60. * Creates a revision in the database.
  61. *
  62. * @param WikiPage $page
  63. * @param $summary
  64. * @param null|string|Content $content
  65. *
  66. * @return RevisionRecord|null
  67. */
  68. private function createRevision( WikiPage $page, $summary, $content = null ) {
  69. $user = $this->getTestUser()->getUser();
  70. $comment = CommentStoreComment::newUnsavedComment( $summary );
  71. if ( $content === null || is_string( $content ) ) {
  72. $content = new WikitextContent( $content ?? $summary );
  73. }
  74. if ( !is_array( $content ) ) {
  75. $content = [ 'main' => $content ];
  76. }
  77. $this->getDerivedPageDataUpdater( $page ); // flush cached instance before.
  78. $updater = $page->newPageUpdater( $user );
  79. foreach ( $content as $role => $c ) {
  80. $updater->setContent( $role, $c );
  81. }
  82. $rev = $updater->saveRevision( $comment );
  83. $this->getDerivedPageDataUpdater( $page ); // flush cached instance after.
  84. return $rev;
  85. }
  86. // TODO: test setArticleCountMethod() and isCountable();
  87. // TODO: test isRedirect() and wasRedirect()
  88. /**
  89. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOptions()
  90. */
  91. public function testGetCanonicalParserOptions() {
  92. $user = $this->getTestUser()->getUser();
  93. $page = $this->getPage( __METHOD__ );
  94. $parentRev = $this->createRevision( $page, 'first' );
  95. $mainContent = new WikitextContent( 'Lorem ipsum' );
  96. $update = new RevisionSlotsUpdate();
  97. $update->modifyContent( 'main', $mainContent );
  98. $updater = $this->getDerivedPageDataUpdater( $page );
  99. $updater->prepareContent( $user, $update, false );
  100. $options1 = $updater->getCanonicalParserOptions();
  101. $this->assertSame( MediaWikiServices::getInstance()->getContentLanguage(),
  102. $options1->getUserLangObj() );
  103. $speculativeId = $options1->getSpeculativeRevId();
  104. $this->assertSame( $parentRev->getId() + 1, $speculativeId );
  105. $rev = $this->makeRevision(
  106. $page->getTitle(),
  107. $update,
  108. $user,
  109. $parentRev->getId() + 7,
  110. $parentRev->getId()
  111. );
  112. $updater->prepareUpdate( $rev );
  113. $options2 = $updater->getCanonicalParserOptions();
  114. $currentRev = call_user_func( $options2->getCurrentRevisionCallback(), $page->getTitle() );
  115. $this->assertSame( $rev->getId(), $currentRev->getId() );
  116. }
  117. /**
  118. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::grabCurrentRevision()
  119. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
  120. */
  121. public function testGrabCurrentRevision() {
  122. $page = $this->getPage( __METHOD__ );
  123. $updater0 = $this->getDerivedPageDataUpdater( $page );
  124. $this->assertNull( $updater0->grabCurrentRevision() );
  125. $this->assertFalse( $updater0->pageExisted() );
  126. $rev1 = $this->createRevision( $page, 'first' );
  127. $updater1 = $this->getDerivedPageDataUpdater( $page );
  128. $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
  129. $this->assertFalse( $updater0->pageExisted() );
  130. $this->assertTrue( $updater1->pageExisted() );
  131. $rev2 = $this->createRevision( $page, 'second' );
  132. $updater2 = $this->getDerivedPageDataUpdater( $page );
  133. $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
  134. $this->assertSame( $rev2->getId(), $updater2->grabCurrentRevision()->getId() );
  135. }
  136. /**
  137. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
  138. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isContentPrepared()
  139. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
  140. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
  141. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
  142. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
  143. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
  144. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
  145. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
  146. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
  147. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
  148. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
  149. */
  150. public function testPrepareContent() {
  151. $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
  152. $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
  153. $this->assertFalse( $updater->isContentPrepared() );
  154. // TODO: test stash
  155. // TODO: MCR: Test multiple slots. Test slot removal.
  156. $mainContent = new WikitextContent( 'first [[main]] ~~~' );
  157. $auxContent = new WikitextContent( 'inherited ~~~ content' );
  158. $auxSlot = SlotRecord::newSaved(
  159. 10, 7, 'tt:7',
  160. SlotRecord::newUnsaved( 'aux', $auxContent )
  161. );
  162. $update = new RevisionSlotsUpdate();
  163. $update->modifyContent( 'main', $mainContent );
  164. $update->modifySlot( SlotRecord::newInherited( $auxSlot ) );
  165. // TODO: MCR: test removing slots!
  166. $updater->prepareContent( $sysop, $update, false );
  167. // second be ok to call again with the same params
  168. $updater->prepareContent( $sysop, $update, false );
  169. $this->assertNull( $updater->grabCurrentRevision() );
  170. $this->assertTrue( $updater->isContentPrepared() );
  171. $this->assertFalse( $updater->isUpdatePrepared() );
  172. $this->assertFalse( $updater->pageExisted() );
  173. $this->assertTrue( $updater->isCreation() );
  174. $this->assertTrue( $updater->isChange() );
  175. $this->assertFalse( $updater->isContentDeleted() );
  176. $this->assertNotNull( $updater->getRevision() );
  177. $this->assertNotNull( $updater->getRenderedRevision() );
  178. $this->assertEquals( [ 'main', 'aux' ], $updater->getSlots()->getSlotRoles() );
  179. $this->assertEquals( [ 'main' ], array_keys( $updater->getSlots()->getOriginalSlots() ) );
  180. $this->assertEquals( [ 'aux' ], array_keys( $updater->getSlots()->getInheritedSlots() ) );
  181. $this->assertEquals( [ 'main', 'aux' ], $updater->getModifiedSlotRoles() );
  182. $this->assertEquals( [ 'main', 'aux' ], $updater->getTouchedSlotRoles() );
  183. $mainSlot = $updater->getRawSlot( 'main' );
  184. $this->assertInstanceOf( SlotRecord::class, $mainSlot );
  185. $this->assertNotContains( '~~~', $mainSlot->getContent()->serialize(), 'PST should apply.' );
  186. $this->assertContains( $sysop->getName(), $mainSlot->getContent()->serialize() );
  187. $auxSlot = $updater->getRawSlot( 'aux' );
  188. $this->assertInstanceOf( SlotRecord::class, $auxSlot );
  189. $this->assertContains( '~~~', $auxSlot->getContent()->serialize(), 'No PST should apply.' );
  190. $mainOutput = $updater->getCanonicalParserOutput();
  191. $this->assertContains( 'first', $mainOutput->getText() );
  192. $this->assertContains( '<a ', $mainOutput->getText() );
  193. $this->assertNotEmpty( $mainOutput->getLinks() );
  194. $canonicalOutput = $updater->getCanonicalParserOutput();
  195. $this->assertContains( 'first', $canonicalOutput->getText() );
  196. $this->assertContains( '<a ', $canonicalOutput->getText() );
  197. $this->assertContains( 'inherited ', $canonicalOutput->getText() );
  198. $this->assertNotEmpty( $canonicalOutput->getLinks() );
  199. }
  200. /**
  201. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareContent()
  202. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::pageExisted()
  203. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
  204. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isChange()
  205. */
  206. public function testPrepareContentInherit() {
  207. $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
  208. $page = $this->getPage( __METHOD__ );
  209. $mainContent1 = new WikitextContent( 'first [[main]] ({{REVISIONUSER}}) #~~~#' );
  210. $mainContent2 = new WikitextContent( 'second ({{subst:REVISIONUSER}}) #~~~#' );
  211. $rev = $this->createRevision( $page, 'first', $mainContent1 );
  212. $mainContent1 = $rev->getContent( 'main' ); // get post-pst content
  213. $userName = $rev->getUser()->getName();
  214. $sysopName = $sysop->getName();
  215. $update = new RevisionSlotsUpdate();
  216. $update->modifyContent( 'main', $mainContent1 );
  217. $updater1 = $this->getDerivedPageDataUpdater( $page );
  218. $updater1->prepareContent( $sysop, $update, false );
  219. $this->assertNotNull( $updater1->grabCurrentRevision() );
  220. $this->assertTrue( $updater1->isContentPrepared() );
  221. $this->assertTrue( $updater1->pageExisted() );
  222. $this->assertFalse( $updater1->isCreation() );
  223. $this->assertFalse( $updater1->isChange() );
  224. $this->assertNotNull( $updater1->getRevision() );
  225. $this->assertNotNull( $updater1->getRenderedRevision() );
  226. // parser-output for null-edit uses the original author's name
  227. $html = $updater1->getRenderedRevision()->getRevisionParserOutput()->getText();
  228. $this->assertNotContains( $sysopName, $html, '{{REVISIONUSER}}' );
  229. $this->assertNotContains( '{{REVISIONUSER}}', $html, '{{REVISIONUSER}}' );
  230. $this->assertNotContains( '~~~', $html, 'signature ~~~' );
  231. $this->assertContains( '(' . $userName . ')', $html, '{{REVISIONUSER}}' );
  232. $this->assertContains( '>' . $userName . '<', $html, 'signature ~~~' );
  233. // TODO: MCR: test inheritance from parent
  234. $update = new RevisionSlotsUpdate();
  235. $update->modifyContent( 'main', $mainContent2 );
  236. $updater2 = $this->getDerivedPageDataUpdater( $page );
  237. $updater2->prepareContent( $sysop, $update, false );
  238. // non-null edit use the new user name in PST
  239. $pstText = $updater2->getSlots()->getContent( 'main' )->serialize();
  240. $this->assertNotContains( '{{subst:REVISIONUSER}}', $pstText, '{{subst:REVISIONUSER}}' );
  241. $this->assertNotContains( '~~~', $pstText, 'signature ~~~' );
  242. $this->assertContains( '(' . $sysopName . ')', $pstText, '{{subst:REVISIONUSER}}' );
  243. $this->assertContains( ':' . $sysopName . '|', $pstText, 'signature ~~~' );
  244. $this->assertFalse( $updater2->isCreation() );
  245. $this->assertTrue( $updater2->isChange() );
  246. }
  247. // TODO: test failure of prepareContent() when called again...
  248. // - with different user
  249. // - with different update
  250. // - after calling prepareUpdate()
  251. /**
  252. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
  253. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isUpdatePrepared()
  254. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isCreation()
  255. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlots()
  256. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawSlot()
  257. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getRawContent()
  258. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getModifiedSlotRoles()
  259. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getTouchedSlotRoles()
  260. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
  261. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOutput()
  262. */
  263. public function testPrepareUpdate() {
  264. $page = $this->getPage( __METHOD__ );
  265. $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
  266. $rev1 = $this->createRevision( $page, 'first', $mainContent1 );
  267. $updater1 = $this->getDerivedPageDataUpdater( $page, $rev1 );
  268. $options = []; // TODO: test *all* the options...
  269. $updater1->prepareUpdate( $rev1, $options );
  270. $this->assertTrue( $updater1->isUpdatePrepared() );
  271. $this->assertTrue( $updater1->isContentPrepared() );
  272. $this->assertTrue( $updater1->isCreation() );
  273. $this->assertTrue( $updater1->isChange() );
  274. $this->assertFalse( $updater1->isContentDeleted() );
  275. $this->assertNotNull( $updater1->getRevision() );
  276. $this->assertNotNull( $updater1->getRenderedRevision() );
  277. $this->assertEquals( [ 'main' ], $updater1->getSlots()->getSlotRoles() );
  278. $this->assertEquals( [ 'main' ], array_keys( $updater1->getSlots()->getOriginalSlots() ) );
  279. $this->assertEquals( [], array_keys( $updater1->getSlots()->getInheritedSlots() ) );
  280. $this->assertEquals( [ 'main' ], $updater1->getModifiedSlotRoles() );
  281. $this->assertEquals( [ 'main' ], $updater1->getTouchedSlotRoles() );
  282. // TODO: MCR: test multiple slots, test slot removal!
  283. $this->assertInstanceOf( SlotRecord::class, $updater1->getRawSlot( 'main' ) );
  284. $this->assertNotContains( '~~~~', $updater1->getRawContent( 'main' )->serialize() );
  285. $mainOutput = $updater1->getCanonicalParserOutput();
  286. $this->assertContains( 'first', $mainOutput->getText() );
  287. $this->assertContains( '<a ', $mainOutput->getText() );
  288. $this->assertNotEmpty( $mainOutput->getLinks() );
  289. $canonicalOutput = $updater1->getCanonicalParserOutput();
  290. $this->assertContains( 'first', $canonicalOutput->getText() );
  291. $this->assertContains( '<a ', $canonicalOutput->getText() );
  292. $this->assertNotEmpty( $canonicalOutput->getLinks() );
  293. $mainContent2 = new WikitextContent( 'second' );
  294. $rev2 = $this->createRevision( $page, 'second', $mainContent2 );
  295. $updater2 = $this->getDerivedPageDataUpdater( $page, $rev2 );
  296. $options = []; // TODO: test *all* the options...
  297. $updater2->prepareUpdate( $rev2, $options );
  298. $this->assertFalse( $updater2->isCreation() );
  299. $this->assertTrue( $updater2->isChange() );
  300. $canonicalOutput = $updater2->getCanonicalParserOutput();
  301. $this->assertContains( 'second', $canonicalOutput->getText() );
  302. }
  303. /**
  304. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
  305. */
  306. public function testPrepareUpdateReusesParserOutput() {
  307. $user = $this->getTestUser()->getUser();
  308. $page = $this->getPage( __METHOD__ );
  309. $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
  310. $update = new RevisionSlotsUpdate();
  311. $update->modifyContent( 'main', $mainContent1 );
  312. $updater = $this->getDerivedPageDataUpdater( $page );
  313. $updater->prepareContent( $user, $update, false );
  314. $mainOutput = $updater->getSlotParserOutput( 'main' );
  315. $canonicalOutput = $updater->getCanonicalParserOutput();
  316. $rev = $this->createRevision( $page, 'first', $mainContent1 );
  317. $options = []; // TODO: test *all* the options...
  318. $updater->prepareUpdate( $rev, $options );
  319. $this->assertTrue( $updater->isUpdatePrepared() );
  320. $this->assertTrue( $updater->isContentPrepared() );
  321. $this->assertSame( $mainOutput, $updater->getSlotParserOutput( 'main' ) );
  322. $this->assertSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
  323. }
  324. /**
  325. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
  326. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getSlotParserOutput()
  327. */
  328. public function testPrepareUpdateOutputReset() {
  329. $user = $this->getTestUser()->getUser();
  330. $page = $this->getPage( __METHOD__ );
  331. $mainContent1 = new WikitextContent( 'first --{{REVISIONID}}--' );
  332. $update = new RevisionSlotsUpdate();
  333. $update->modifyContent( 'main', $mainContent1 );
  334. $updater = $this->getDerivedPageDataUpdater( $page );
  335. $updater->prepareContent( $user, $update, false );
  336. $mainOutput = $updater->getSlotParserOutput( 'main' );
  337. $canonicalOutput = $updater->getCanonicalParserOutput();
  338. // prevent optimization on matching speculative ID
  339. $mainOutput->setSpeculativeRevIdUsed( 0 );
  340. $canonicalOutput->setSpeculativeRevIdUsed( 0 );
  341. $rev = $this->createRevision( $page, 'first', $mainContent1 );
  342. $options = []; // TODO: test *all* the options...
  343. $updater->prepareUpdate( $rev, $options );
  344. $this->assertTrue( $updater->isUpdatePrepared() );
  345. $this->assertTrue( $updater->isContentPrepared() );
  346. // ParserOutput objects should have been flushed.
  347. $this->assertNotSame( $mainOutput, $updater->getSlotParserOutput( 'main' ) );
  348. $this->assertNotSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
  349. $html = $updater->getCanonicalParserOutput()->getText();
  350. $this->assertContains( '--' . $rev->getId() . '--', $html );
  351. // TODO: MCR: ensure that when the main slot uses {{REVISIONID}} but another slot is
  352. // updated, the main slot is still re-rendered!
  353. }
  354. // TODO: test failure of prepareUpdate() when called again with a different revision
  355. // TODO: test failure of prepareUpdate() on inconsistency with prepareContent.
  356. /**
  357. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
  358. */
  359. public function testGetPreparedEditAfterPrepareContent() {
  360. $user = $this->getTestUser()->getUser();
  361. $mainContent = new WikitextContent( 'first [[main]] ~~~' );
  362. $update = new RevisionSlotsUpdate();
  363. $update->modifyContent( 'main', $mainContent );
  364. $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
  365. $updater->prepareContent( $user, $update, false );
  366. $canonicalOutput = $updater->getCanonicalParserOutput();
  367. $preparedEdit = $updater->getPreparedEdit();
  368. $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
  369. $this->assertSame( $canonicalOutput, $preparedEdit->output );
  370. $this->assertSame( $mainContent, $preparedEdit->newContent );
  371. $this->assertSame( $updater->getRawContent( 'main' ), $preparedEdit->pstContent );
  372. $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
  373. $this->assertSame( null, $preparedEdit->revid );
  374. }
  375. /**
  376. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
  377. */
  378. public function testGetPreparedEditAfterPrepareUpdate() {
  379. $page = $this->getPage( __METHOD__ );
  380. $mainContent = new WikitextContent( 'first [[main]] ~~~' );
  381. $update = new MutableRevisionSlots();
  382. $update->setContent( 'main', $mainContent );
  383. $rev = $this->createRevision( $page, __METHOD__ );
  384. $updater = $this->getDerivedPageDataUpdater( $page );
  385. $updater->prepareUpdate( $rev );
  386. $canonicalOutput = $updater->getCanonicalParserOutput();
  387. $preparedEdit = $updater->getPreparedEdit();
  388. $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
  389. $this->assertSame( $canonicalOutput, $preparedEdit->output );
  390. $this->assertSame( $updater->getRawContent( 'main' ), $preparedEdit->pstContent );
  391. $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
  392. $this->assertSame( $rev->getId(), $preparedEdit->revid );
  393. }
  394. public function testGetSecondaryDataUpdatesAfterPrepareContent() {
  395. $user = $this->getTestUser()->getUser();
  396. $page = $this->getPage( __METHOD__ );
  397. $this->createRevision( $page, __METHOD__ );
  398. $mainContent1 = new WikitextContent( 'first' );
  399. $update = new RevisionSlotsUpdate();
  400. $update->modifyContent( 'main', $mainContent1 );
  401. $updater = $this->getDerivedPageDataUpdater( $page );
  402. $updater->prepareContent( $user, $update, false );
  403. $dataUpdates = $updater->getSecondaryDataUpdates();
  404. $this->assertNotEmpty( $dataUpdates );
  405. $linksUpdates = array_filter( $dataUpdates, function ( $du ) {
  406. return $du instanceof LinksUpdate;
  407. } );
  408. $this->assertCount( 1, $linksUpdates );
  409. }
  410. /**
  411. * @param string $name
  412. *
  413. * @return ContentHandler
  414. */
  415. private function defineMockContentModelForUpdateTesting( $name ) {
  416. /** @var ContentHandler|MockObject $handler */
  417. $handler = $this->getMockBuilder( TextContentHandler::class )
  418. ->setConstructorArgs( [ $name ] )
  419. ->setMethods(
  420. [ 'getSecondaryDataUpdates', 'getDeletionUpdates', 'unserializeContent' ]
  421. )
  422. ->getMock();
  423. $dataUpdate = new MWCallableUpdate( 'time' );
  424. $dataUpdate->_name = "$name data update";
  425. $deletionUpdate = new MWCallableUpdate( 'time' );
  426. $deletionUpdate->_name = "$name deletion update";
  427. $handler->method( 'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] );
  428. $handler->method( 'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] );
  429. $handler->method( 'unserializeContent' )->willReturnCallback(
  430. function ( $text ) use ( $handler ) {
  431. return $this->createMockContent( $handler, $text );
  432. }
  433. );
  434. $this->mergeMwGlobalArrayValue(
  435. 'wgContentHandlers', [
  436. $name => function () use ( $handler ){
  437. return $handler;
  438. }
  439. ]
  440. );
  441. return $handler;
  442. }
  443. /**
  444. * @param ContentHandler $handler
  445. * @param string $text
  446. *
  447. * @return Content
  448. */
  449. private function createMockContent( ContentHandler $handler, $text ) {
  450. /** @var Content|MockObject $content */
  451. $content = $this->getMockBuilder( TextContent::class )
  452. ->setConstructorArgs( [ $text ] )
  453. ->setMethods( [ 'getModel', 'getContentHandler' ] )
  454. ->getMock();
  455. $content->method( 'getModel' )->willReturn( $handler->getModelID() );
  456. $content->method( 'getContentHandler' )->willReturn( $handler );
  457. return $content;
  458. }
  459. public function testGetSecondaryDataUpdatesWithSlotRemoval() {
  460. global $wgMultiContentRevisionSchemaMigrationStage;
  461. if ( ! ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW ) ) {
  462. $this->markTestSkipped( 'Slot removal cannot happen with MCR being enabled' );
  463. }
  464. $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
  465. $a1 = $this->defineMockContentModelForUpdateTesting( 'A1' );
  466. $m2 = $this->defineMockContentModelForUpdateTesting( 'M2' );
  467. $mainContent1 = $this->createMockContent( $m1, 'main 1' );
  468. $auxContent1 = $this->createMockContent( $a1, 'aux 1' );
  469. $mainContent2 = $this->createMockContent( $m2, 'main 2' );
  470. $user = $this->getTestUser()->getUser();
  471. $page = $this->getPage( __METHOD__ );
  472. $this->createRevision(
  473. $page,
  474. __METHOD__,
  475. [ 'main' => $mainContent1, 'aux' => $auxContent1 ]
  476. );
  477. $update = new RevisionSlotsUpdate();
  478. $update->modifyContent( 'main', $mainContent2 );
  479. $update->removeSlot( 'aux' );
  480. $page = $this->getPage( __METHOD__ );
  481. $updater = $this->getDerivedPageDataUpdater( $page );
  482. $updater->prepareContent( $user, $update, false );
  483. $dataUpdates = $updater->getSecondaryDataUpdates();
  484. $this->assertNotEmpty( $dataUpdates );
  485. $updateNames = array_map( function ( $du ) {
  486. return isset( $du->_name ) ? $du->_name : get_class( $du );
  487. }, $dataUpdates );
  488. $this->assertContains( LinksUpdate::class, $updateNames );
  489. $this->assertContains( 'A1 deletion update', $updateNames );
  490. $this->assertContains( 'M2 data update', $updateNames );
  491. $this->assertNotContains( 'M1 data update', $updateNames );
  492. }
  493. /**
  494. * Creates a dummy revision object without touching the database.
  495. *
  496. * @param Title $title
  497. * @param RevisionSlotsUpdate $update
  498. * @param User $user
  499. * @param string $comment
  500. * @param int $id
  501. * @param int $parentId
  502. *
  503. * @return MutableRevisionRecord
  504. */
  505. private function makeRevision(
  506. Title $title,
  507. RevisionSlotsUpdate $update,
  508. User $user,
  509. $comment,
  510. $id,
  511. $parentId = 0
  512. ) {
  513. $rev = new MutableRevisionRecord( $title );
  514. $rev->applyUpdate( $update );
  515. $rev->setUser( $user );
  516. $rev->setComment( CommentStoreComment::newUnsavedComment( $comment ) );
  517. $rev->setId( $id );
  518. $rev->setPageId( $title->getArticleID() );
  519. $rev->setParentId( $parentId );
  520. return $rev;
  521. }
  522. /**
  523. * @param int $id
  524. * @return Title
  525. */
  526. private function getMockTitle( $id = 23 ) {
  527. $mock = $this->getMockBuilder( Title::class )
  528. ->disableOriginalConstructor()
  529. ->getMock();
  530. $mock->expects( $this->any() )
  531. ->method( 'getDBkey' )
  532. ->will( $this->returnValue( __CLASS__ ) );
  533. $mock->expects( $this->any() )
  534. ->method( 'getArticleID' )
  535. ->will( $this->returnValue( $id ) );
  536. return $mock;
  537. }
  538. public function provideIsReusableFor() {
  539. $title = $this->getMockTitle();
  540. $user1 = User::newFromName( 'Alice' );
  541. $user2 = User::newFromName( 'Bob' );
  542. $content1 = new WikitextContent( 'one' );
  543. $content2 = new WikitextContent( 'two' );
  544. $update1 = new RevisionSlotsUpdate();
  545. $update1->modifyContent( 'main', $content1 );
  546. $update1b = new RevisionSlotsUpdate();
  547. $update1b->modifyContent( 'xyz', $content1 );
  548. $update2 = new RevisionSlotsUpdate();
  549. $update2->modifyContent( 'main', $content2 );
  550. $rev1 = $this->makeRevision( $title, $update1, $user1, 'rev1', 11 );
  551. $rev1b = $this->makeRevision( $title, $update1b, $user1, 'rev1', 11 );
  552. $rev2 = $this->makeRevision( $title, $update2, $user1, 'rev2', 12 );
  553. $rev2x = $this->makeRevision( $title, $update2, $user2, 'rev2', 12 );
  554. $rev2y = $this->makeRevision( $title, $update2, $user1, 'rev2', 122 );
  555. yield 'any' => [
  556. '$prepUser' => null,
  557. '$prepRevision' => null,
  558. '$prepUpdate' => null,
  559. '$forUser' => null,
  560. '$forRevision' => null,
  561. '$forUpdate' => null,
  562. '$forParent' => null,
  563. '$isReusable' => true,
  564. ];
  565. yield 'for any' => [
  566. '$prepUser' => $user1,
  567. '$prepRevision' => $rev1,
  568. '$prepUpdate' => $update1,
  569. '$forUser' => null,
  570. '$forRevision' => null,
  571. '$forUpdate' => null,
  572. '$forParent' => null,
  573. '$isReusable' => true,
  574. ];
  575. yield 'unprepared' => [
  576. '$prepUser' => null,
  577. '$prepRevision' => null,
  578. '$prepUpdate' => null,
  579. '$forUser' => $user1,
  580. '$forRevision' => $rev1,
  581. '$forUpdate' => $update1,
  582. '$forParent' => 0,
  583. '$isReusable' => true,
  584. ];
  585. yield 'match prepareContent' => [
  586. '$prepUser' => $user1,
  587. '$prepRevision' => null,
  588. '$prepUpdate' => $update1,
  589. '$forUser' => $user1,
  590. '$forRevision' => null,
  591. '$forUpdate' => $update1,
  592. '$forParent' => 0,
  593. '$isReusable' => true,
  594. ];
  595. yield 'match prepareUpdate' => [
  596. '$prepUser' => null,
  597. '$prepRevision' => $rev1,
  598. '$prepUpdate' => null,
  599. '$forUser' => $user1,
  600. '$forRevision' => $rev1,
  601. '$forUpdate' => null,
  602. '$forParent' => 0,
  603. '$isReusable' => true,
  604. ];
  605. yield 'match all' => [
  606. '$prepUser' => $user1,
  607. '$prepRevision' => $rev1,
  608. '$prepUpdate' => $update1,
  609. '$forUser' => $user1,
  610. '$forRevision' => $rev1,
  611. '$forUpdate' => $update1,
  612. '$forParent' => 0,
  613. '$isReusable' => true,
  614. ];
  615. yield 'mismatch prepareContent update' => [
  616. '$prepUser' => $user1,
  617. '$prepRevision' => null,
  618. '$prepUpdate' => $update1,
  619. '$forUser' => $user1,
  620. '$forRevision' => null,
  621. '$forUpdate' => $update1b,
  622. '$forParent' => 0,
  623. '$isReusable' => false,
  624. ];
  625. yield 'mismatch prepareContent user' => [
  626. '$prepUser' => $user1,
  627. '$prepRevision' => null,
  628. '$prepUpdate' => $update1,
  629. '$forUser' => $user2,
  630. '$forRevision' => null,
  631. '$forUpdate' => $update1,
  632. '$forParent' => 0,
  633. '$isReusable' => false,
  634. ];
  635. yield 'mismatch prepareContent parent' => [
  636. '$prepUser' => $user1,
  637. '$prepRevision' => null,
  638. '$prepUpdate' => $update1,
  639. '$forUser' => $user1,
  640. '$forRevision' => null,
  641. '$forUpdate' => $update1,
  642. '$forParent' => 7,
  643. '$isReusable' => false,
  644. ];
  645. yield 'mismatch prepareUpdate revision update' => [
  646. '$prepUser' => null,
  647. '$prepRevision' => $rev1,
  648. '$prepUpdate' => null,
  649. '$forUser' => null,
  650. '$forRevision' => $rev1b,
  651. '$forUpdate' => null,
  652. '$forParent' => 0,
  653. '$isReusable' => false,
  654. ];
  655. yield 'mismatch prepareUpdate revision user' => [
  656. '$prepUser' => null,
  657. '$prepRevision' => $rev2,
  658. '$prepUpdate' => null,
  659. '$forUser' => null,
  660. '$forRevision' => $rev2x,
  661. '$forUpdate' => null,
  662. '$forParent' => 0,
  663. '$isReusable' => false,
  664. ];
  665. yield 'mismatch prepareUpdate revision id' => [
  666. '$prepUser' => null,
  667. '$prepRevision' => $rev2,
  668. '$prepUpdate' => null,
  669. '$forUser' => null,
  670. '$forRevision' => $rev2y,
  671. '$forUpdate' => null,
  672. '$forParent' => 0,
  673. '$isReusable' => false,
  674. ];
  675. }
  676. /**
  677. * @dataProvider provideIsReusableFor
  678. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::isReusableFor()
  679. *
  680. * @param User|null $prepUser
  681. * @param RevisionRecord|null $prepRevision
  682. * @param RevisionSlotsUpdate|null $prepUpdate
  683. * @param User|null $forUser
  684. * @param RevisionRecord|null $forRevision
  685. * @param RevisionSlotsUpdate|null $forUpdate
  686. * @param int|null $forParent
  687. * @param bool $isReusable
  688. */
  689. public function testIsReusableFor(
  690. User $prepUser = null,
  691. RevisionRecord $prepRevision = null,
  692. RevisionSlotsUpdate $prepUpdate = null,
  693. User $forUser = null,
  694. RevisionRecord $forRevision = null,
  695. RevisionSlotsUpdate $forUpdate = null,
  696. $forParent = null,
  697. $isReusable = null
  698. ) {
  699. $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
  700. if ( $prepUpdate ) {
  701. $updater->prepareContent( $prepUser, $prepUpdate, false );
  702. }
  703. if ( $prepRevision ) {
  704. $updater->prepareUpdate( $prepRevision );
  705. }
  706. $this->assertSame(
  707. $isReusable,
  708. $updater->isReusableFor( $forUser, $forRevision, $forUpdate, $forParent )
  709. );
  710. }
  711. /**
  712. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doUpdates()
  713. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doSecondaryDataUpdates()
  714. * @covers \MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
  715. */
  716. public function testDoUpdates() {
  717. $page = $this->getPage( __METHOD__ );
  718. $content = [ 'main' => new WikitextContent( 'first [[main]]' ) ];
  719. if ( $this->hasMultiSlotSupport() ) {
  720. $content['aux'] = new WikitextContent( 'Aux [[Nix]]' );
  721. }
  722. $rev = $this->createRevision( $page, 'first', $content );
  723. $pageId = $page->getId();
  724. $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
  725. $this->db->delete( 'pagelinks', '*' );
  726. $pcache = MediaWikiServices::getInstance()->getParserCache();
  727. $pcache->deleteOptionsKey( $page );
  728. $updater = $this->getDerivedPageDataUpdater( $page, $rev );
  729. $updater->setArticleCountMethod( 'link' );
  730. $options = []; // TODO: test *all* the options...
  731. $updater->prepareUpdate( $rev, $options );
  732. $updater->doUpdates();
  733. // links table update
  734. $pageLinks = $this->db->select(
  735. 'pagelinks',
  736. '*',
  737. [ 'pl_from' => $pageId ],
  738. __METHOD__,
  739. [ 'ORDER BY' => 'pl_namespace, pl_title' ]
  740. );
  741. $pageLinksRow = $pageLinks->fetchObject();
  742. $this->assertInternalType( 'object', $pageLinksRow );
  743. $this->assertSame( 'Main', $pageLinksRow->pl_title );
  744. if ( $this->hasMultiSlotSupport() ) {
  745. $pageLinksRow = $pageLinks->fetchObject();
  746. $this->assertInternalType( 'object', $pageLinksRow );
  747. $this->assertSame( 'Nix', $pageLinksRow->pl_title );
  748. }
  749. // parser cache update
  750. $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() );
  751. $this->assertInternalType( 'object', $cached );
  752. $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
  753. // site stats
  754. $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
  755. $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
  756. $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
  757. $this->assertSame( $oldStats->ss_good_articles + 1, (int)$stats->ss_good_articles );
  758. // TODO: MCR: test data updates for additional slots!
  759. // TODO: test update for edit without page creation
  760. // TODO: test message cache purge
  761. // TODO: test module cache purge
  762. // TODO: test CDN purge
  763. // TODO: test newtalk update
  764. // TODO: test search update
  765. // TODO: test site stats good_articles while turning the page into (or back from) a redir.
  766. // TODO: test category membership update (with setRcWatchCategoryMembership())
  767. }
  768. private function hasMultiSlotSupport() {
  769. global $wgMultiContentRevisionSchemaMigrationStage;
  770. return ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW )
  771. && ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW );
  772. }
  773. }