MediaWiki  master
DerivedPageDataUpdaterTest.php
Go to the documentation of this file.
1 <?php
2 
4 
22 use Title;
23 use User;
28 
35 
36  public function tearDown() {
37  MWTimestamp::setFakeTime( false );
38 
39  parent::tearDown();
40  }
41 
47  private function getTitle( $title ) {
49  }
50 
56  private function getPage( $title ) {
57  $title = ( $title instanceof Title ) ? $title : $this->getTitle( $title );
58 
59  return WikiPage::factory( $title );
60  }
61 
69  private function getDerivedPageDataUpdater(
70  $page, RevisionRecord $rec = null, User $user = null
71  ) {
72  if ( is_string( $page ) || $page instanceof Title ) {
73  $page = $this->getPage( $page );
74  }
75 
76  $page = TestingAccessWrapper::newFromObject( $page );
77  return $page->getDerivedDataUpdater( $user, $rec );
78  }
79 
90  private function createRevision( WikiPage $page, $summary, $content = null, $user = null ) {
91  $user = $user ?: $this->getTestUser()->getUser();
92  $comment = CommentStoreComment::newUnsavedComment( $summary );
93 
94  if ( $content === null || is_string( $content ) ) {
95  $content = new WikitextContent( $content ?? $summary );
96  }
97 
98  if ( !is_array( $content ) ) {
99  $content = [ 'main' => $content ];
100  }
101 
102  $this->getDerivedPageDataUpdater( $page ); // flush cached instance before.
103 
104  $updater = $page->newPageUpdater( $user );
105 
106  foreach ( $content as $role => $c ) {
107  $updater->setContent( $role, $c );
108  }
109 
110  $rev = $updater->saveRevision( $comment );
111  if ( !$updater->wasSuccessful() ) {
112  $this->fail( $updater->getStatus()->getWikiText() );
113  }
114 
115  $this->getDerivedPageDataUpdater( $page ); // flush cached instance after.
116  return $rev;
117  }
118 
119  // TODO: test setArticleCountMethod() and isCountable();
120  // TODO: test isRedirect() and wasRedirect()
121 
125  public function testGetCanonicalParserOptions() {
126  $user = $this->getTestUser()->getUser();
127  $page = $this->getPage( __METHOD__ );
128 
129  $parentRev = $this->createRevision( $page, 'first' );
130 
131  $mainContent = new WikitextContent( 'Lorem ipsum' );
132 
133  $update = new RevisionSlotsUpdate();
134  $update->modifyContent( SlotRecord::MAIN, $mainContent );
135  $updater = $this->getDerivedPageDataUpdater( $page );
136  $updater->prepareContent( $user, $update, false );
137 
138  $options1 = $updater->getCanonicalParserOptions();
139  $this->assertSame( MediaWikiServices::getInstance()->getContentLanguage(),
140  $options1->getUserLangObj() );
141 
142  $speculativeId = $options1->getSpeculativeRevId();
143  $this->assertSame( $parentRev->getId() + 1, $speculativeId );
144 
145  $rev = $this->makeRevision(
146  $page->getTitle(),
147  $update,
148  $user,
149  $parentRev->getId() + 7,
150  $parentRev->getId()
151  );
152  $updater->prepareUpdate( $rev );
153 
154  $options2 = $updater->getCanonicalParserOptions();
155 
156  $currentRev = call_user_func( $options2->getCurrentRevisionCallback(), $page->getTitle() );
157  $this->assertSame( $rev->getId(), $currentRev->getId() );
158  }
159 
164  public function testGrabCurrentRevision() {
165  $page = $this->getPage( __METHOD__ );
166 
167  $updater0 = $this->getDerivedPageDataUpdater( $page );
168  $this->assertNull( $updater0->grabCurrentRevision() );
169  $this->assertFalse( $updater0->pageExisted() );
170 
171  $rev1 = $this->createRevision( $page, 'first' );
172  $updater1 = $this->getDerivedPageDataUpdater( $page );
173  $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
174  $this->assertFalse( $updater0->pageExisted() );
175  $this->assertTrue( $updater1->pageExisted() );
176 
177  $rev2 = $this->createRevision( $page, 'second' );
178  $updater2 = $this->getDerivedPageDataUpdater( $page );
179  $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
180  $this->assertSame( $rev2->getId(), $updater2->grabCurrentRevision()->getId() );
181  }
182 
197  public function testPrepareContent() {
198  MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
199  'aux',
201  );
202 
203  $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
204  $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
205 
206  $this->assertFalse( $updater->isContentPrepared() );
207 
208  // TODO: test stash
209  // TODO: MCR: Test multiple slots. Test slot removal.
210  $mainContent = new WikitextContent( 'first [[main]] ~~~' );
211  $auxContent = new WikitextContent( 'inherited ~~~ content' );
212  $auxSlot = SlotRecord::newSaved(
213  10, 7, 'tt:7',
214  SlotRecord::newUnsaved( 'aux', $auxContent )
215  );
216 
217  $update = new RevisionSlotsUpdate();
218  $update->modifyContent( SlotRecord::MAIN, $mainContent );
219  $update->modifySlot( SlotRecord::newInherited( $auxSlot ) );
220  // TODO: MCR: test removing slots!
221 
222  $updater->prepareContent( $sysop, $update, false );
223 
224  // second be ok to call again with the same params
225  $updater->prepareContent( $sysop, $update, false );
226 
227  $this->assertNull( $updater->grabCurrentRevision() );
228  $this->assertTrue( $updater->isContentPrepared() );
229  $this->assertFalse( $updater->isUpdatePrepared() );
230  $this->assertFalse( $updater->pageExisted() );
231  $this->assertTrue( $updater->isCreation() );
232  $this->assertTrue( $updater->isChange() );
233  $this->assertFalse( $updater->isContentDeleted() );
234 
235  $this->assertNotNull( $updater->getRevision() );
236  $this->assertNotNull( $updater->getRenderedRevision() );
237 
238  $this->assertEquals( [ 'main', 'aux' ], $updater->getSlots()->getSlotRoles() );
239  $this->assertEquals( [ 'main' ], array_keys( $updater->getSlots()->getOriginalSlots() ) );
240  $this->assertEquals( [ 'aux' ], array_keys( $updater->getSlots()->getInheritedSlots() ) );
241  $this->assertEquals( [ 'main', 'aux' ], $updater->getModifiedSlotRoles() );
242  $this->assertEquals( [ 'main', 'aux' ], $updater->getTouchedSlotRoles() );
243 
244  $mainSlot = $updater->getRawSlot( SlotRecord::MAIN );
245  $this->assertInstanceOf( SlotRecord::class, $mainSlot );
246  $this->assertNotContains( '~~~', $mainSlot->getContent()->serialize(), 'PST should apply.' );
247  $this->assertContains( $sysop->getName(), $mainSlot->getContent()->serialize() );
248 
249  $auxSlot = $updater->getRawSlot( 'aux' );
250  $this->assertInstanceOf( SlotRecord::class, $auxSlot );
251  $this->assertContains( '~~~', $auxSlot->getContent()->serialize(), 'No PST should apply.' );
252 
253  $mainOutput = $updater->getCanonicalParserOutput();
254  $this->assertContains( 'first', $mainOutput->getText() );
255  $this->assertContains( '<a ', $mainOutput->getText() );
256  $this->assertNotEmpty( $mainOutput->getLinks() );
257 
258  $canonicalOutput = $updater->getCanonicalParserOutput();
259  $this->assertContains( 'first', $canonicalOutput->getText() );
260  $this->assertContains( '<a ', $canonicalOutput->getText() );
261  $this->assertContains( 'inherited ', $canonicalOutput->getText() );
262  $this->assertNotEmpty( $canonicalOutput->getLinks() );
263  }
264 
271  public function testPrepareContentInherit() {
272  $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
273  $page = $this->getPage( __METHOD__ );
274 
275  $mainContent1 = new WikitextContent( 'first [[main]] ({{REVISIONUSER}}) #~~~#' );
276  $mainContent2 = new WikitextContent( 'second ({{subst:REVISIONUSER}}) #~~~#' );
277 
278  $rev = $this->createRevision( $page, 'first', $mainContent1 );
279  $mainContent1 = $rev->getContent( SlotRecord::MAIN ); // get post-pst content
280  $userName = $rev->getUser()->getName();
281  $sysopName = $sysop->getName();
282 
283  $update = new RevisionSlotsUpdate();
284  $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
285  $updater1 = $this->getDerivedPageDataUpdater( $page );
286  $updater1->prepareContent( $sysop, $update, false );
287 
288  $this->assertNotNull( $updater1->grabCurrentRevision() );
289  $this->assertTrue( $updater1->isContentPrepared() );
290  $this->assertTrue( $updater1->pageExisted() );
291  $this->assertFalse( $updater1->isCreation() );
292  $this->assertFalse( $updater1->isChange() );
293 
294  $this->assertNotNull( $updater1->getRevision() );
295  $this->assertNotNull( $updater1->getRenderedRevision() );
296 
297  // parser-output for null-edit uses the original author's name
298  $html = $updater1->getRenderedRevision()->getRevisionParserOutput()->getText();
299  $this->assertNotContains( $sysopName, $html, '{{REVISIONUSER}}' );
300  $this->assertNotContains( '{{REVISIONUSER}}', $html, '{{REVISIONUSER}}' );
301  $this->assertNotContains( '~~~', $html, 'signature ~~~' );
302  $this->assertContains( '(' . $userName . ')', $html, '{{REVISIONUSER}}' );
303  $this->assertContains( '>' . $userName . '<', $html, 'signature ~~~' );
304 
305  // TODO: MCR: test inheritance from parent
306  $update = new RevisionSlotsUpdate();
307  $update->modifyContent( SlotRecord::MAIN, $mainContent2 );
308  $updater2 = $this->getDerivedPageDataUpdater( $page );
309  $updater2->prepareContent( $sysop, $update, false );
310 
311  // non-null edit use the new user name in PST
312  $pstText = $updater2->getSlots()->getContent( SlotRecord::MAIN )->serialize();
313  $this->assertNotContains( '{{subst:REVISIONUSER}}', $pstText, '{{subst:REVISIONUSER}}' );
314  $this->assertNotContains( '~~~', $pstText, 'signature ~~~' );
315  $this->assertContains( '(' . $sysopName . ')', $pstText, '{{subst:REVISIONUSER}}' );
316  $this->assertContains( ':' . $sysopName . '|', $pstText, 'signature ~~~' );
317 
318  $this->assertFalse( $updater2->isCreation() );
319  $this->assertTrue( $updater2->isChange() );
320  }
321 
322  // TODO: test failure of prepareContent() when called again...
323  // - with different user
324  // - with different update
325  // - after calling prepareUpdate()
326 
339  public function testPrepareUpdate() {
340  $page = $this->getPage( __METHOD__ );
341 
342  $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
343  $rev1 = $this->createRevision( $page, 'first', $mainContent1 );
344  $updater1 = $this->getDerivedPageDataUpdater( $page, $rev1 );
345 
346  $options = []; // TODO: test *all* the options...
347  $updater1->prepareUpdate( $rev1, $options );
348 
349  $this->assertTrue( $updater1->isUpdatePrepared() );
350  $this->assertTrue( $updater1->isContentPrepared() );
351  $this->assertTrue( $updater1->isCreation() );
352  $this->assertTrue( $updater1->isChange() );
353  $this->assertFalse( $updater1->isContentDeleted() );
354 
355  $this->assertNotNull( $updater1->getRevision() );
356  $this->assertNotNull( $updater1->getRenderedRevision() );
357 
358  $this->assertEquals( [ 'main' ], $updater1->getSlots()->getSlotRoles() );
359  $this->assertEquals( [ 'main' ], array_keys( $updater1->getSlots()->getOriginalSlots() ) );
360  $this->assertEquals( [], array_keys( $updater1->getSlots()->getInheritedSlots() ) );
361  $this->assertEquals( [ 'main' ], $updater1->getModifiedSlotRoles() );
362  $this->assertEquals( [ 'main' ], $updater1->getTouchedSlotRoles() );
363 
364  // TODO: MCR: test multiple slots, test slot removal!
365 
366  $this->assertInstanceOf( SlotRecord::class, $updater1->getRawSlot( SlotRecord::MAIN ) );
367  $this->assertNotContains( '~~~~', $updater1->getRawContent( SlotRecord::MAIN )->serialize() );
368 
369  $mainOutput = $updater1->getCanonicalParserOutput();
370  $this->assertContains( 'first', $mainOutput->getText() );
371  $this->assertContains( '<a ', $mainOutput->getText() );
372  $this->assertNotEmpty( $mainOutput->getLinks() );
373 
374  $canonicalOutput = $updater1->getCanonicalParserOutput();
375  $this->assertContains( 'first', $canonicalOutput->getText() );
376  $this->assertContains( '<a ', $canonicalOutput->getText() );
377  $this->assertNotEmpty( $canonicalOutput->getLinks() );
378 
379  $mainContent2 = new WikitextContent( 'second' );
380  $rev2 = $this->createRevision( $page, 'second', $mainContent2 );
381  $updater2 = $this->getDerivedPageDataUpdater( $page, $rev2 );
382 
383  $options = []; // TODO: test *all* the options...
384  $updater2->prepareUpdate( $rev2, $options );
385 
386  $this->assertFalse( $updater2->isCreation() );
387  $this->assertTrue( $updater2->isChange() );
388 
389  $canonicalOutput = $updater2->getCanonicalParserOutput();
390  $this->assertContains( 'second', $canonicalOutput->getText() );
391  }
392 
397  $user = $this->getTestUser()->getUser();
398  $page = $this->getPage( __METHOD__ );
399 
400  $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
401 
402  $update = new RevisionSlotsUpdate();
403  $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
404  $updater = $this->getDerivedPageDataUpdater( $page );
405  $updater->prepareContent( $user, $update, false );
406 
407  $mainOutput = $updater->getSlotParserOutput( SlotRecord::MAIN );
408  $canonicalOutput = $updater->getCanonicalParserOutput();
409 
410  $rev = $this->createRevision( $page, 'first', $mainContent1 );
411 
412  $options = []; // TODO: test *all* the options...
413  $updater->prepareUpdate( $rev, $options );
414 
415  $this->assertTrue( $updater->isUpdatePrepared() );
416  $this->assertTrue( $updater->isContentPrepared() );
417 
418  $this->assertSame( $mainOutput, $updater->getSlotParserOutput( SlotRecord::MAIN ) );
419  $this->assertSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
420  }
421 
426  public function testPrepareUpdateOutputReset() {
427  $user = $this->getTestUser()->getUser();
428  $page = $this->getPage( __METHOD__ );
429 
430  $mainContent1 = new WikitextContent( 'first --{{REVISIONID}}--' );
431 
432  $update = new RevisionSlotsUpdate();
433  $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
434  $updater = $this->getDerivedPageDataUpdater( $page );
435  $updater->prepareContent( $user, $update, false );
436 
437  $mainOutput = $updater->getSlotParserOutput( SlotRecord::MAIN );
438  $canonicalOutput = $updater->getCanonicalParserOutput();
439 
440  // prevent optimization on matching speculative ID
441  $mainOutput->setSpeculativeRevIdUsed( 0 );
442  $canonicalOutput->setSpeculativeRevIdUsed( 0 );
443 
444  $rev = $this->createRevision( $page, 'first', $mainContent1 );
445 
446  $options = []; // TODO: test *all* the options...
447  $updater->prepareUpdate( $rev, $options );
448 
449  $this->assertTrue( $updater->isUpdatePrepared() );
450  $this->assertTrue( $updater->isContentPrepared() );
451 
452  // ParserOutput objects should have been flushed.
453  $this->assertNotSame( $mainOutput, $updater->getSlotParserOutput( SlotRecord::MAIN ) );
454  $this->assertNotSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
455 
456  $html = $updater->getCanonicalParserOutput()->getText();
457  $this->assertContains( '--' . $rev->getId() . '--', $html );
458 
459  // TODO: MCR: ensure that when the main slot uses {{REVISIONID}} but another slot is
460  // updated, the main slot is still re-rendered!
461  }
462 
463  // TODO: test failure of prepareUpdate() when called again with a different revision
464  // TODO: test failure of prepareUpdate() on inconsistency with prepareContent.
465 
470  $user = $this->getTestUser()->getUser();
471 
472  $mainContent = new WikitextContent( 'first [[main]] ~~~' );
473  $update = new RevisionSlotsUpdate();
474  $update->modifyContent( SlotRecord::MAIN, $mainContent );
475 
476  $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
477  $updater->prepareContent( $user, $update, false );
478 
479  $canonicalOutput = $updater->getCanonicalParserOutput();
480 
481  $preparedEdit = $updater->getPreparedEdit();
482  $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
483  $this->assertSame( $canonicalOutput, $preparedEdit->output );
484  $this->assertSame( $mainContent, $preparedEdit->newContent );
485  $this->assertSame( $updater->getRawContent( SlotRecord::MAIN ), $preparedEdit->pstContent );
486  $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
487  $this->assertSame( null, $preparedEdit->revid );
488  }
489 
494  $clock = MWTimestamp::convert( TS_UNIX, '20100101000000' );
495  MWTimestamp::setFakeTime( function () use ( &$clock ) {
496  return $clock++;
497  } );
498 
499  $page = $this->getPage( __METHOD__ );
500 
501  $mainContent = new WikitextContent( 'first [[main]] ~~~' );
502  $update = new MutableRevisionSlots();
503  $update->setContent( SlotRecord::MAIN, $mainContent );
504 
505  $rev = $this->createRevision( $page, __METHOD__ );
506 
507  $updater = $this->getDerivedPageDataUpdater( $page );
508  $updater->prepareUpdate( $rev );
509 
510  $canonicalOutput = $updater->getCanonicalParserOutput();
511 
512  $preparedEdit = $updater->getPreparedEdit();
513  $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
514  $this->assertSame( $canonicalOutput, $preparedEdit->output );
515  $this->assertSame( $updater->getRawContent( SlotRecord::MAIN ), $preparedEdit->pstContent );
516  $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
517  $this->assertSame( $rev->getId(), $preparedEdit->revid );
518  }
519 
521  $user = $this->getTestUser()->getUser();
522  $page = $this->getPage( __METHOD__ );
523  $this->createRevision( $page, __METHOD__ );
524 
525  $mainContent1 = new WikitextContent( 'first' );
526 
527  $update = new RevisionSlotsUpdate();
528  $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
529  $updater = $this->getDerivedPageDataUpdater( $page );
530  $updater->prepareContent( $user, $update, false );
531 
532  $dataUpdates = $updater->getSecondaryDataUpdates();
533 
534  $this->assertNotEmpty( $dataUpdates );
535 
536  $linksUpdates = array_filter( $dataUpdates, function ( $du ) {
537  return $du instanceof LinksUpdate;
538  } );
539  $this->assertCount( 1, $linksUpdates );
540  }
541 
549  $handler = $this->getMockBuilder( TextContentHandler::class )
550  ->setConstructorArgs( [ $name ] )
551  ->setMethods(
552  [ 'getSecondaryDataUpdates', 'getDeletionUpdates', 'unserializeContent' ]
553  )
554  ->getMock();
555 
556  $dataUpdate = new MWCallableUpdate( 'time' );
557  $dataUpdate->_name = "$name data update";
558 
559  $deletionUpdate = new MWCallableUpdate( 'time' );
560  $deletionUpdate->_name = "$name deletion update";
561 
562  $handler->method( 'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] );
563  $handler->method( 'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] );
564  $handler->method( 'unserializeContent' )->willReturnCallback(
565  function ( $text ) use ( $handler ) {
566  return $this->createMockContent( $handler, $text );
567  }
568  );
569 
571  'wgContentHandlers', [
572  $name => function () use ( $handler ){
573  return $handler;
574  }
575  ]
576  );
577 
578  return $handler;
579  }
580 
587  private function createMockContent( ContentHandler $handler, $text ) {
589  $content = $this->getMockBuilder( TextContent::class )
590  ->setConstructorArgs( [ $text ] )
591  ->setMethods( [ 'getModel', 'getContentHandler' ] )
592  ->getMock();
593 
594  $content->method( 'getModel' )->willReturn( $handler->getModelID() );
595  $content->method( 'getContentHandler' )->willReturn( $handler );
596 
597  return $content;
598  }
599 
601  if ( !$this->hasMultiSlotSupport() ) {
602  $this->markTestSkipped( 'Slot removal cannot happen with MCR being enabled' );
603  }
604 
605  $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
606  $a1 = $this->defineMockContentModelForUpdateTesting( 'A1' );
607  $m2 = $this->defineMockContentModelForUpdateTesting( 'M2' );
608 
609  MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
610  'aux',
611  $a1->getModelID()
612  );
613 
614  $mainContent1 = $this->createMockContent( $m1, 'main 1' );
615  $auxContent1 = $this->createMockContent( $a1, 'aux 1' );
616  $mainContent2 = $this->createMockContent( $m2, 'main 2' );
617 
618  $user = $this->getTestUser()->getUser();
619  $page = $this->getPage( __METHOD__ );
620  $this->createRevision(
621  $page,
622  __METHOD__,
623  [ 'main' => $mainContent1, 'aux' => $auxContent1 ]
624  );
625 
626  $update = new RevisionSlotsUpdate();
627  $update->modifyContent( SlotRecord::MAIN, $mainContent2 );
628  $update->removeSlot( 'aux' );
629 
630  $page = $this->getPage( __METHOD__ );
631  $updater = $this->getDerivedPageDataUpdater( $page );
632  $updater->prepareContent( $user, $update, false );
633 
634  $dataUpdates = $updater->getSecondaryDataUpdates();
635 
636  $this->assertNotEmpty( $dataUpdates );
637 
638  $updateNames = array_map( function ( $du ) {
639  return isset( $du->_name ) ? $du->_name : get_class( $du );
640  }, $dataUpdates );
641 
642  $this->assertContains( LinksUpdate::class, $updateNames );
643  $this->assertContains( 'A1 deletion update', $updateNames );
644  $this->assertContains( 'M2 data update', $updateNames );
645  $this->assertNotContains( 'M1 data update', $updateNames );
646  }
647 
660  private function makeRevision(
661  Title $title,
662  RevisionSlotsUpdate $update,
663  User $user,
664  $comment,
665  $id = 0,
666  $parentId = 0
667  ) {
668  $rev = new MutableRevisionRecord( $title );
669 
670  $rev->applyUpdate( $update );
671  $rev->setUser( $user );
672  $rev->setComment( CommentStoreComment::newUnsavedComment( $comment ) );
673  $rev->setPageId( $title->getArticleID() );
674  $rev->setParentId( $parentId );
675 
676  if ( $id ) {
677  $rev->setId( $id );
678  }
679 
680  return $rev;
681  }
682 
687  private function getMockTitle( $id = 23 ) {
688  $mock = $this->getMockBuilder( Title::class )
689  ->disableOriginalConstructor()
690  ->getMock();
691  $mock->expects( $this->any() )
692  ->method( 'getDBkey' )
693  ->will( $this->returnValue( __CLASS__ ) );
694  $mock->expects( $this->any() )
695  ->method( 'getArticleID' )
696  ->will( $this->returnValue( $id ) );
697 
698  return $mock;
699  }
700 
701  public function provideIsReusableFor() {
702  $title = $this->getMockTitle();
703 
704  $user1 = User::newFromName( 'Alice' );
705  $user2 = User::newFromName( 'Bob' );
706 
707  $content1 = new WikitextContent( 'one' );
708  $content2 = new WikitextContent( 'two' );
709 
710  $update1 = new RevisionSlotsUpdate();
711  $update1->modifyContent( SlotRecord::MAIN, $content1 );
712 
713  $update1b = new RevisionSlotsUpdate();
714  $update1b->modifyContent( 'xyz', $content1 );
715 
716  $update2 = new RevisionSlotsUpdate();
717  $update2->modifyContent( SlotRecord::MAIN, $content2 );
718 
719  $rev1 = $this->makeRevision( $title, $update1, $user1, 'rev1', 11 );
720  $rev1b = $this->makeRevision( $title, $update1b, $user1, 'rev1', 11 );
721 
722  $rev2 = $this->makeRevision( $title, $update2, $user1, 'rev2', 12 );
723  $rev2x = $this->makeRevision( $title, $update2, $user2, 'rev2', 12 );
724  $rev2y = $this->makeRevision( $title, $update2, $user1, 'rev2', 122 );
725 
726  yield 'any' => [
727  '$prepUser' => null,
728  '$prepRevision' => null,
729  '$prepUpdate' => null,
730  '$forUser' => null,
731  '$forRevision' => null,
732  '$forUpdate' => null,
733  '$forParent' => null,
734  '$isReusable' => true,
735  ];
736  yield 'for any' => [
737  '$prepUser' => $user1,
738  '$prepRevision' => $rev1,
739  '$prepUpdate' => $update1,
740  '$forUser' => null,
741  '$forRevision' => null,
742  '$forUpdate' => null,
743  '$forParent' => null,
744  '$isReusable' => true,
745  ];
746  yield 'unprepared' => [
747  '$prepUser' => null,
748  '$prepRevision' => null,
749  '$prepUpdate' => null,
750  '$forUser' => $user1,
751  '$forRevision' => $rev1,
752  '$forUpdate' => $update1,
753  '$forParent' => 0,
754  '$isReusable' => true,
755  ];
756  yield 'match prepareContent' => [
757  '$prepUser' => $user1,
758  '$prepRevision' => null,
759  '$prepUpdate' => $update1,
760  '$forUser' => $user1,
761  '$forRevision' => null,
762  '$forUpdate' => $update1,
763  '$forParent' => 0,
764  '$isReusable' => true,
765  ];
766  yield 'match prepareUpdate' => [
767  '$prepUser' => null,
768  '$prepRevision' => $rev1,
769  '$prepUpdate' => null,
770  '$forUser' => $user1,
771  '$forRevision' => $rev1,
772  '$forUpdate' => null,
773  '$forParent' => 0,
774  '$isReusable' => true,
775  ];
776  yield 'match all' => [
777  '$prepUser' => $user1,
778  '$prepRevision' => $rev1,
779  '$prepUpdate' => $update1,
780  '$forUser' => $user1,
781  '$forRevision' => $rev1,
782  '$forUpdate' => $update1,
783  '$forParent' => 0,
784  '$isReusable' => true,
785  ];
786  yield 'mismatch prepareContent update' => [
787  '$prepUser' => $user1,
788  '$prepRevision' => null,
789  '$prepUpdate' => $update1,
790  '$forUser' => $user1,
791  '$forRevision' => null,
792  '$forUpdate' => $update1b,
793  '$forParent' => 0,
794  '$isReusable' => false,
795  ];
796  yield 'mismatch prepareContent user' => [
797  '$prepUser' => $user1,
798  '$prepRevision' => null,
799  '$prepUpdate' => $update1,
800  '$forUser' => $user2,
801  '$forRevision' => null,
802  '$forUpdate' => $update1,
803  '$forParent' => 0,
804  '$isReusable' => false,
805  ];
806  yield 'mismatch prepareContent parent' => [
807  '$prepUser' => $user1,
808  '$prepRevision' => null,
809  '$prepUpdate' => $update1,
810  '$forUser' => $user1,
811  '$forRevision' => null,
812  '$forUpdate' => $update1,
813  '$forParent' => 7,
814  '$isReusable' => false,
815  ];
816  yield 'mismatch prepareUpdate revision update' => [
817  '$prepUser' => null,
818  '$prepRevision' => $rev1,
819  '$prepUpdate' => null,
820  '$forUser' => null,
821  '$forRevision' => $rev1b,
822  '$forUpdate' => null,
823  '$forParent' => 0,
824  '$isReusable' => false,
825  ];
826  yield 'mismatch prepareUpdate revision id' => [
827  '$prepUser' => null,
828  '$prepRevision' => $rev2,
829  '$prepUpdate' => null,
830  '$forUser' => null,
831  '$forRevision' => $rev2y,
832  '$forUpdate' => null,
833  '$forParent' => 0,
834  '$isReusable' => false,
835  ];
836  }
837 
851  public function testIsReusableFor(
852  User $prepUser = null,
853  RevisionRecord $prepRevision = null,
854  RevisionSlotsUpdate $prepUpdate = null,
855  User $forUser = null,
856  RevisionRecord $forRevision = null,
857  RevisionSlotsUpdate $forUpdate = null,
858  $forParent = null,
859  $isReusable = null
860  ) {
861  $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
862 
863  if ( $prepUpdate ) {
864  $updater->prepareContent( $prepUser, $prepUpdate, false );
865  }
866 
867  if ( $prepRevision ) {
868  $updater->prepareUpdate( $prepRevision );
869  }
870 
871  $this->assertSame(
872  $isReusable,
873  $updater->isReusableFor( $forUser, $forRevision, $forUpdate, $forParent )
874  );
875  }
876 
882  public function testDoUpdates() {
883  $page = $this->getPage( __METHOD__ );
884 
885  $content = [ 'main' => new WikitextContent( 'first [[main]]' ) ];
886 
887  if ( $this->hasMultiSlotSupport() ) {
888  $content['aux'] = new WikitextContent( 'Aux [[Nix]]' );
889 
890  MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
891  'aux',
893  );
894  }
895 
896  $rev = $this->createRevision( $page, 'first', $content );
897  $pageId = $page->getId();
898 
899  $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
900  $this->db->delete( 'pagelinks', '*' );
901 
902  $pcache = MediaWikiServices::getInstance()->getParserCache();
903  $pcache->deleteOptionsKey( $page );
904 
905  $updater = $this->getDerivedPageDataUpdater( $page, $rev );
906  $updater->setArticleCountMethod( 'link' );
907 
908  $options = []; // TODO: test *all* the options...
909  $updater->prepareUpdate( $rev, $options );
910 
911  $updater->doUpdates();
912 
913  // links table update
914  $pageLinks = $this->db->select(
915  'pagelinks',
916  '*',
917  [ 'pl_from' => $pageId ],
918  __METHOD__,
919  [ 'ORDER BY' => 'pl_namespace, pl_title' ]
920  );
921 
922  $pageLinksRow = $pageLinks->fetchObject();
923  $this->assertInternalType( 'object', $pageLinksRow );
924  $this->assertSame( 'Main', $pageLinksRow->pl_title );
925 
926  if ( $this->hasMultiSlotSupport() ) {
927  $pageLinksRow = $pageLinks->fetchObject();
928  $this->assertInternalType( 'object', $pageLinksRow );
929  $this->assertSame( 'Nix', $pageLinksRow->pl_title );
930  }
931 
932  // parser cache update
933  $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() );
934  $this->assertInternalType( 'object', $cached );
935  $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
936 
937  // site stats
938  $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
939  $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
940  $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
941  $this->assertSame( $oldStats->ss_good_articles + 1, (int)$stats->ss_good_articles );
942 
943  // TODO: MCR: test data updates for additional slots!
944  // TODO: test update for edit without page creation
945  // TODO: test message cache purge
946  // TODO: test module cache purge
947  // TODO: test CDN purge
948  // TODO: test newtalk update
949  // TODO: test search update
950  // TODO: test site stats good_articles while turning the page into (or back from) a redir.
951  // TODO: test category membership update (with setRcWatchCategoryMembership())
952  }
953 
960  $page = $this->getPage( __METHOD__ );
961 
962  // Case where user has canonical parser options
963  $content = [ 'main' => new WikitextContent( 'rev ID ver #1: {{REVISIONID}}' ) ];
964  $rev = $this->createRevision( $page, 'first', $content );
965  $pcache = MediaWikiServices::getInstance()->getParserCache();
966  $pcache->deleteOptionsKey( $page );
967 
968  $this->db->startAtomic( __METHOD__ ); // let deferred updates queue up
969 
970  $updater = $this->getDerivedPageDataUpdater( $page, $rev );
971  $updater->prepareUpdate( $rev, [] );
972  $updater->doUpdates();
973 
974  $this->assertGreaterThan( 0, DeferredUpdates::pendingUpdatesCount(), 'Pending updates' );
975  $this->assertNotFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
976 
977  $this->db->endAtomic( __METHOD__ ); // run deferred updates
978 
979  $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' );
980  }
981 
988  $page = $this->getPage( __METHOD__ );
989 
990  // Case where user does not have canonical parser options
991  $user = $this->getMutableTestUser()->getUser();
992  $user->setOption(
993  'thumbsize',
994  $user->getOption( 'thumbsize' ) + 1
995  );
996  $content = [ 'main' => new WikitextContent( 'rev ID ver #2: {{REVISIONID}}' ) ];
997  $rev = $this->createRevision( $page, 'first', $content, $user );
998  $pcache = MediaWikiServices::getInstance()->getParserCache();
999  $pcache->deleteOptionsKey( $page );
1000 
1001  $this->db->startAtomic( __METHOD__ ); // let deferred updates queue up
1002 
1003  $updater = $this->getDerivedPageDataUpdater( $page, $rev, $user );
1004  $updater->prepareUpdate( $rev, [] );
1005  $updater->doUpdates();
1006 
1007  $this->assertGreaterThan( 1, DeferredUpdates::pendingUpdatesCount(), 'Pending updates' );
1008  $this->assertFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
1009 
1010  $this->db->endAtomic( __METHOD__ ); // run deferred updates
1011 
1012  $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount(), 'No pending updates' );
1013  $this->assertNotFalse( $pcache->get( $page, $updater->getCanonicalParserOptions() ) );
1014  }
1015 
1019  public function testDoParserCacheUpdate() {
1020  if ( $this->hasMultiSlotSupport() ) {
1021  MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
1022  'aux',
1024  );
1025  }
1026 
1027  $page = $this->getPage( __METHOD__ );
1028  $this->createRevision( $page, 'Dummy' );
1029 
1030  $user = $this->getTestUser()->getUser();
1031 
1032  $update = new RevisionSlotsUpdate();
1033  $update->modifyContent( 'main', new WikitextContent( 'first [[Main]]' ) );
1034 
1035  if ( $this->hasMultiSlotSupport() ) {
1036  $update->modifyContent( 'aux', new WikitextContent( 'Aux [[Nix]]' ) );
1037  }
1038 
1039  // Emulate update after edit ----------
1040  $pcache = MediaWikiServices::getInstance()->getParserCache();
1041  $pcache->deleteOptionsKey( $page );
1042 
1043  $rev = $this->makeRevision( $page->getTitle(), $update, $user, 'rev', null );
1044  $rev->setTimestamp( '20100101000000' );
1045  $rev->setParentId( $page->getLatest() );
1046 
1047  $updater = $this->getDerivedPageDataUpdater( $page );
1048  $updater->prepareContent( $user, $update, false );
1049 
1050  $rev->setId( 11 );
1051  $updater->prepareUpdate( $rev );
1052 
1053  // Force the page timestamp, so we notice whether ParserOutput::getTimestamp
1054  // or ParserOutput::getCacheTime are used.
1055  $page->setTimestamp( $rev->getTimestamp() );
1056  $updater->doParserCacheUpdate();
1057 
1058  // The cached ParserOutput should not use the revision timestamp
1059  $cached = $pcache->get( $page, $updater->getCanonicalParserOptions(), true );
1060  $this->assertInternalType( 'object', $cached );
1061  $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
1062 
1063  $this->assertSame( $rev->getTimestamp(), $cached->getCacheTime() );
1064  $this->assertSame( $rev->getId(), $cached->getCacheRevisionId() );
1065 
1066  // Emulate forced update of an old revision ----------
1067  $pcache->deleteOptionsKey( $page );
1068 
1069  $updater = $this->getDerivedPageDataUpdater( $page );
1070  $updater->prepareUpdate( $rev );
1071 
1072  // Force the page timestamp, so we notice whether ParserOutput::getTimestamp
1073  // or ParserOutput::getCacheTime are used.
1074  $page->setTimestamp( $rev->getTimestamp() );
1075  $updater->doParserCacheUpdate();
1076 
1077  // The cached ParserOutput should not use the revision timestamp
1078  $cached = $pcache->get( $page, $updater->getCanonicalParserOptions(), true );
1079  $this->assertInternalType( 'object', $cached );
1080  $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
1081 
1082  $this->assertGreaterThan( $rev->getTimestamp(), $cached->getCacheTime() );
1083  $this->assertSame( $rev->getId(), $cached->getCacheRevisionId() );
1084  }
1085 
1089  private function hasMultiSlotSupport() {
1091 
1092  return ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_WRITE_NEW )
1093  && ( $wgMultiContentRevisionSchemaMigrationStage & SCHEMA_COMPAT_READ_NEW );
1094  }
1095 
1096 }
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:138
testGetPreparedEditAfterPrepareContent()
\MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
testDoUpdates()
\MediaWiki\Storage\DerivedPageDataUpdater::doUpdates() \MediaWiki\Storage\DerivedPageDataUpdater::doS...
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1982
static newSaved( $revisionId, $contentId, $contentAddress, SlotRecord $protoSlot)
Constructs a complete SlotRecord for a newly saved revision, based on the incomplete proto-slot...
Definition: SlotRecord.php:164
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3005
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:231
int $wgMultiContentRevisionSchemaMigrationStage
RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables)...
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:283
getModelID()
Returns the model id that identifies the content model this ContentHandler can handle.
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
Value object representing a modification of revision slots.
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
testPrepareUpdate()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate() \MediaWiki\Storage\DerivedPageDataUpdater:...
testGetCanonicalParserOptions()
\MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOptions()
static getInstance()
Returns the global default instance of the top level service locator.
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
This document provides an overview of the usage of PageUpdater and DerivedPageDataUpdater
Definition: pageupdater.txt:3
Mutable RevisionRecord implementation, for building new revision entries programmatically.
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output modifiable modifiable after all normalizations have been except for the $wgMaxImageArea check set to true or false to override the $wgMaxImageArea check result gives extension the possibility to transform it themselves $handler
Definition: hooks.txt:780
getDefaultWikitextNS()
Returns the ID of a namespace that defaults to Wikitext.
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition: hooks.txt:1982
static newInherited(SlotRecord $slot)
Constructs a new SlotRecord for a new revision, inheriting the content of the given SlotRecord of a p...
Definition: SlotRecord.php:103
static pendingUpdatesCount()
newPageUpdater(User $user, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Definition: WikiPage.php:1789
testDoUpdatesCacheSaveDeferral_canonical()
\MediaWiki\Storage\DerivedPageDataUpdater::doUpdates() \MediaWiki\Storage\DerivedPageDataUpdater::doS...
testGetPreparedEditAfterPrepareUpdate()
\MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
createRevision(WikiPage $page, $summary, $content=null, $user=null)
Creates a revision in the database.
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:282
static getMutableTestUser( $groups=[])
Convenience method for getting a mutable test user.
makeRevision(Title $title, RevisionSlotsUpdate $update, User $user, $comment, $id=0, $parentId=0)
Creates a dummy revision object without touching the database.
testPrepareUpdateOutputReset()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate() \MediaWiki\Storage\DerivedPageDataUpdater:...
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1982
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:925
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1766
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:617
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
testPrepareContent()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareContent() \MediaWiki\Storage\DerivedPageDataUpdater...
you have access to all of the normal MediaWiki so you can get a DB use the etc For full docs on the Maintenance class
Definition: maintenance.txt:52
testIsReusableFor(User $prepUser=null, RevisionRecord $prepRevision=null, RevisionSlotsUpdate $prepUpdate=null, User $forUser=null, RevisionRecord $forRevision=null, RevisionSlotsUpdate $forUpdate=null, $forParent=null, $isReusable=null)
provideIsReusableFor \MediaWiki\Storage\DerivedPageDataUpdater::isReusableFor()
$page->newPageUpdater($user) $updater
Definition: pageupdater.txt:63
testPrepareUpdateReusesParserOutput()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
static newUnsaved( $role, Content $content)
Constructs a new Slot from a Content object for a new revision.
Definition: SlotRecord.php:129
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
testDoParserCacheUpdate()
\MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
Page revision base class.
getDerivedPageDataUpdater( $page, RevisionRecord $rec=null, User $user=null)
Mutable version of RevisionSlots, for constructing a new revision.
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition: User.php:594
$content
Definition: pageupdater.txt:72
testGrabCurrentRevision()
\MediaWiki\Storage\DerivedPageDataUpdater::grabCurrentRevision() \MediaWiki\Storage\DerivedPageDataUp...
testPrepareContentInherit()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareContent() \MediaWiki\Storage\DerivedPageDataUpdater...
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
testDoUpdatesCacheSaveDeferral_noncanonical()
\MediaWiki\Storage\DerivedPageDataUpdater::doUpdates() \MediaWiki\Storage\DerivedPageDataUpdater::doS...
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1473