MediaWiki REL1_33
DerivedPageDataUpdaterTest.php
Go to the documentation of this file.
1<?php
2
4
19use PHPUnit\Framework\MockObject\MockObject;
23use User;
24use Wikimedia\TestingAccessWrapper;
27
34
35 public function tearDown() {
36 MWTimestamp::setFakeTime( false );
37
38 parent::tearDown();
39 }
40
46 private function getTitle( $title ) {
47 return Title::makeTitleSafe( $this->getDefaultWikitextNS(), $title );
48 }
49
55 private function getPage( $title ) {
56 $title = ( $title instanceof Title ) ? $title : $this->getTitle( $title );
57
58 return WikiPage::factory( $title );
59 }
60
66 private function getDerivedPageDataUpdater( $page, RevisionRecord $rec = null ) {
67 if ( is_string( $page ) || $page instanceof Title ) {
68 $page = $this->getPage( $page );
69 }
70
71 $page = TestingAccessWrapper::newFromObject( $page );
72 return $page->getDerivedDataUpdater( null, $rec );
73 }
74
84 private function createRevision( WikiPage $page, $summary, $content = null ) {
85 $user = $this->getTestUser()->getUser();
86 $comment = CommentStoreComment::newUnsavedComment( $summary );
87
88 if ( $content === null || is_string( $content ) ) {
89 $content = new WikitextContent( $content ?? $summary );
90 }
91
92 if ( !is_array( $content ) ) {
93 $content = [ 'main' => $content ];
94 }
95
96 $this->getDerivedPageDataUpdater( $page ); // flush cached instance before.
97
98 $updater = $page->newPageUpdater( $user );
99
100 foreach ( $content as $role => $c ) {
101 $updater->setContent( $role, $c );
102 }
103
104 $rev = $updater->saveRevision( $comment );
105 if ( !$updater->wasSuccessful() ) {
106 $this->fail( $updater->getStatus()->getWikiText() );
107 }
108
109 $this->getDerivedPageDataUpdater( $page ); // flush cached instance after.
110 return $rev;
111 }
112
113 // TODO: test setArticleCountMethod() and isCountable();
114 // TODO: test isRedirect() and wasRedirect()
115
120 $user = $this->getTestUser()->getUser();
121 $page = $this->getPage( __METHOD__ );
122
123 $parentRev = $this->createRevision( $page, 'first' );
124
125 $mainContent = new WikitextContent( 'Lorem ipsum' );
126
127 $update = new RevisionSlotsUpdate();
128 $update->modifyContent( SlotRecord::MAIN, $mainContent );
129 $updater = $this->getDerivedPageDataUpdater( $page );
130 $updater->prepareContent( $user, $update, false );
131
132 $options1 = $updater->getCanonicalParserOptions();
133 $this->assertSame( MediaWikiServices::getInstance()->getContentLanguage(),
134 $options1->getUserLangObj() );
135
136 $speculativeId = $options1->getSpeculativeRevId();
137 $this->assertSame( $parentRev->getId() + 1, $speculativeId );
138
139 $rev = $this->makeRevision(
140 $page->getTitle(),
141 $update,
142 $user,
143 $parentRev->getId() + 7,
144 $parentRev->getId()
145 );
146 $updater->prepareUpdate( $rev );
147
148 $options2 = $updater->getCanonicalParserOptions();
149
150 $currentRev = call_user_func( $options2->getCurrentRevisionCallback(), $page->getTitle() );
151 $this->assertSame( $rev->getId(), $currentRev->getId() );
152 }
153
158 public function testGrabCurrentRevision() {
159 $page = $this->getPage( __METHOD__ );
160
161 $updater0 = $this->getDerivedPageDataUpdater( $page );
162 $this->assertNull( $updater0->grabCurrentRevision() );
163 $this->assertFalse( $updater0->pageExisted() );
164
165 $rev1 = $this->createRevision( $page, 'first' );
166 $updater1 = $this->getDerivedPageDataUpdater( $page );
167 $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
168 $this->assertFalse( $updater0->pageExisted() );
169 $this->assertTrue( $updater1->pageExisted() );
170
171 $rev2 = $this->createRevision( $page, 'second' );
172 $updater2 = $this->getDerivedPageDataUpdater( $page );
173 $this->assertSame( $rev1->getId(), $updater1->grabCurrentRevision()->getId() );
174 $this->assertSame( $rev2->getId(), $updater2->grabCurrentRevision()->getId() );
175 }
176
191 public function testPrepareContent() {
192 MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
193 'aux',
195 );
196
197 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
198 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
199
200 $this->assertFalse( $updater->isContentPrepared() );
201
202 // TODO: test stash
203 // TODO: MCR: Test multiple slots. Test slot removal.
204 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
205 $auxContent = new WikitextContent( 'inherited ~~~ content' );
206 $auxSlot = SlotRecord::newSaved(
207 10, 7, 'tt:7',
208 SlotRecord::newUnsaved( 'aux', $auxContent )
209 );
210
211 $update = new RevisionSlotsUpdate();
212 $update->modifyContent( SlotRecord::MAIN, $mainContent );
213 $update->modifySlot( SlotRecord::newInherited( $auxSlot ) );
214 // TODO: MCR: test removing slots!
215
216 $updater->prepareContent( $sysop, $update, false );
217
218 // second be ok to call again with the same params
219 $updater->prepareContent( $sysop, $update, false );
220
221 $this->assertNull( $updater->grabCurrentRevision() );
222 $this->assertTrue( $updater->isContentPrepared() );
223 $this->assertFalse( $updater->isUpdatePrepared() );
224 $this->assertFalse( $updater->pageExisted() );
225 $this->assertTrue( $updater->isCreation() );
226 $this->assertTrue( $updater->isChange() );
227 $this->assertFalse( $updater->isContentDeleted() );
228
229 $this->assertNotNull( $updater->getRevision() );
230 $this->assertNotNull( $updater->getRenderedRevision() );
231
232 $this->assertEquals( [ 'main', 'aux' ], $updater->getSlots()->getSlotRoles() );
233 $this->assertEquals( [ 'main' ], array_keys( $updater->getSlots()->getOriginalSlots() ) );
234 $this->assertEquals( [ 'aux' ], array_keys( $updater->getSlots()->getInheritedSlots() ) );
235 $this->assertEquals( [ 'main', 'aux' ], $updater->getModifiedSlotRoles() );
236 $this->assertEquals( [ 'main', 'aux' ], $updater->getTouchedSlotRoles() );
237
238 $mainSlot = $updater->getRawSlot( SlotRecord::MAIN );
239 $this->assertInstanceOf( SlotRecord::class, $mainSlot );
240 $this->assertNotContains( '~~~', $mainSlot->getContent()->serialize(), 'PST should apply.' );
241 $this->assertContains( $sysop->getName(), $mainSlot->getContent()->serialize() );
242
243 $auxSlot = $updater->getRawSlot( 'aux' );
244 $this->assertInstanceOf( SlotRecord::class, $auxSlot );
245 $this->assertContains( '~~~', $auxSlot->getContent()->serialize(), 'No PST should apply.' );
246
247 $mainOutput = $updater->getCanonicalParserOutput();
248 $this->assertContains( 'first', $mainOutput->getText() );
249 $this->assertContains( '<a ', $mainOutput->getText() );
250 $this->assertNotEmpty( $mainOutput->getLinks() );
251
252 $canonicalOutput = $updater->getCanonicalParserOutput();
253 $this->assertContains( 'first', $canonicalOutput->getText() );
254 $this->assertContains( '<a ', $canonicalOutput->getText() );
255 $this->assertContains( 'inherited ', $canonicalOutput->getText() );
256 $this->assertNotEmpty( $canonicalOutput->getLinks() );
257 }
258
265 public function testPrepareContentInherit() {
266 $sysop = $this->getTestUser( [ 'sysop' ] )->getUser();
267 $page = $this->getPage( __METHOD__ );
268
269 $mainContent1 = new WikitextContent( 'first [[main]] ({{REVISIONUSER}}) #~~~#' );
270 $mainContent2 = new WikitextContent( 'second ({{subst:REVISIONUSER}}) #~~~#' );
271
272 $rev = $this->createRevision( $page, 'first', $mainContent1 );
273 $mainContent1 = $rev->getContent( SlotRecord::MAIN ); // get post-pst content
274 $userName = $rev->getUser()->getName();
275 $sysopName = $sysop->getName();
276
277 $update = new RevisionSlotsUpdate();
278 $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
279 $updater1 = $this->getDerivedPageDataUpdater( $page );
280 $updater1->prepareContent( $sysop, $update, false );
281
282 $this->assertNotNull( $updater1->grabCurrentRevision() );
283 $this->assertTrue( $updater1->isContentPrepared() );
284 $this->assertTrue( $updater1->pageExisted() );
285 $this->assertFalse( $updater1->isCreation() );
286 $this->assertFalse( $updater1->isChange() );
287
288 $this->assertNotNull( $updater1->getRevision() );
289 $this->assertNotNull( $updater1->getRenderedRevision() );
290
291 // parser-output for null-edit uses the original author's name
292 $html = $updater1->getRenderedRevision()->getRevisionParserOutput()->getText();
293 $this->assertNotContains( $sysopName, $html, '{{REVISIONUSER}}' );
294 $this->assertNotContains( '{{REVISIONUSER}}', $html, '{{REVISIONUSER}}' );
295 $this->assertNotContains( '~~~', $html, 'signature ~~~' );
296 $this->assertContains( '(' . $userName . ')', $html, '{{REVISIONUSER}}' );
297 $this->assertContains( '>' . $userName . '<', $html, 'signature ~~~' );
298
299 // TODO: MCR: test inheritance from parent
300 $update = new RevisionSlotsUpdate();
301 $update->modifyContent( SlotRecord::MAIN, $mainContent2 );
302 $updater2 = $this->getDerivedPageDataUpdater( $page );
303 $updater2->prepareContent( $sysop, $update, false );
304
305 // non-null edit use the new user name in PST
306 $pstText = $updater2->getSlots()->getContent( SlotRecord::MAIN )->serialize();
307 $this->assertNotContains( '{{subst:REVISIONUSER}}', $pstText, '{{subst:REVISIONUSER}}' );
308 $this->assertNotContains( '~~~', $pstText, 'signature ~~~' );
309 $this->assertContains( '(' . $sysopName . ')', $pstText, '{{subst:REVISIONUSER}}' );
310 $this->assertContains( ':' . $sysopName . '|', $pstText, 'signature ~~~' );
311
312 $this->assertFalse( $updater2->isCreation() );
313 $this->assertTrue( $updater2->isChange() );
314 }
315
316 // TODO: test failure of prepareContent() when called again...
317 // - with different user
318 // - with different update
319 // - after calling prepareUpdate()
320
333 public function testPrepareUpdate() {
334 $page = $this->getPage( __METHOD__ );
335
336 $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
337 $rev1 = $this->createRevision( $page, 'first', $mainContent1 );
338 $updater1 = $this->getDerivedPageDataUpdater( $page, $rev1 );
339
340 $options = []; // TODO: test *all* the options...
341 $updater1->prepareUpdate( $rev1, $options );
342
343 $this->assertTrue( $updater1->isUpdatePrepared() );
344 $this->assertTrue( $updater1->isContentPrepared() );
345 $this->assertTrue( $updater1->isCreation() );
346 $this->assertTrue( $updater1->isChange() );
347 $this->assertFalse( $updater1->isContentDeleted() );
348
349 $this->assertNotNull( $updater1->getRevision() );
350 $this->assertNotNull( $updater1->getRenderedRevision() );
351
352 $this->assertEquals( [ 'main' ], $updater1->getSlots()->getSlotRoles() );
353 $this->assertEquals( [ 'main' ], array_keys( $updater1->getSlots()->getOriginalSlots() ) );
354 $this->assertEquals( [], array_keys( $updater1->getSlots()->getInheritedSlots() ) );
355 $this->assertEquals( [ 'main' ], $updater1->getModifiedSlotRoles() );
356 $this->assertEquals( [ 'main' ], $updater1->getTouchedSlotRoles() );
357
358 // TODO: MCR: test multiple slots, test slot removal!
359
360 $this->assertInstanceOf( SlotRecord::class, $updater1->getRawSlot( SlotRecord::MAIN ) );
361 $this->assertNotContains( '~~~~', $updater1->getRawContent( SlotRecord::MAIN )->serialize() );
362
363 $mainOutput = $updater1->getCanonicalParserOutput();
364 $this->assertContains( 'first', $mainOutput->getText() );
365 $this->assertContains( '<a ', $mainOutput->getText() );
366 $this->assertNotEmpty( $mainOutput->getLinks() );
367
368 $canonicalOutput = $updater1->getCanonicalParserOutput();
369 $this->assertContains( 'first', $canonicalOutput->getText() );
370 $this->assertContains( '<a ', $canonicalOutput->getText() );
371 $this->assertNotEmpty( $canonicalOutput->getLinks() );
372
373 $mainContent2 = new WikitextContent( 'second' );
374 $rev2 = $this->createRevision( $page, 'second', $mainContent2 );
375 $updater2 = $this->getDerivedPageDataUpdater( $page, $rev2 );
376
377 $options = []; // TODO: test *all* the options...
378 $updater2->prepareUpdate( $rev2, $options );
379
380 $this->assertFalse( $updater2->isCreation() );
381 $this->assertTrue( $updater2->isChange() );
382
383 $canonicalOutput = $updater2->getCanonicalParserOutput();
384 $this->assertContains( 'second', $canonicalOutput->getText() );
385 }
386
391 $user = $this->getTestUser()->getUser();
392 $page = $this->getPage( __METHOD__ );
393
394 $mainContent1 = new WikitextContent( 'first [[main]] ~~~' );
395
396 $update = new RevisionSlotsUpdate();
397 $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
398 $updater = $this->getDerivedPageDataUpdater( $page );
399 $updater->prepareContent( $user, $update, false );
400
401 $mainOutput = $updater->getSlotParserOutput( SlotRecord::MAIN );
402 $canonicalOutput = $updater->getCanonicalParserOutput();
403
404 $rev = $this->createRevision( $page, 'first', $mainContent1 );
405
406 $options = []; // TODO: test *all* the options...
407 $updater->prepareUpdate( $rev, $options );
408
409 $this->assertTrue( $updater->isUpdatePrepared() );
410 $this->assertTrue( $updater->isContentPrepared() );
411
412 $this->assertSame( $mainOutput, $updater->getSlotParserOutput( SlotRecord::MAIN ) );
413 $this->assertSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
414 }
415
421 $user = $this->getTestUser()->getUser();
422 $page = $this->getPage( __METHOD__ );
423
424 $mainContent1 = new WikitextContent( 'first --{{REVISIONID}}--' );
425
426 $update = new RevisionSlotsUpdate();
427 $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
428 $updater = $this->getDerivedPageDataUpdater( $page );
429 $updater->prepareContent( $user, $update, false );
430
431 $mainOutput = $updater->getSlotParserOutput( SlotRecord::MAIN );
432 $canonicalOutput = $updater->getCanonicalParserOutput();
433
434 // prevent optimization on matching speculative ID
435 $mainOutput->setSpeculativeRevIdUsed( 0 );
436 $canonicalOutput->setSpeculativeRevIdUsed( 0 );
437
438 $rev = $this->createRevision( $page, 'first', $mainContent1 );
439
440 $options = []; // TODO: test *all* the options...
441 $updater->prepareUpdate( $rev, $options );
442
443 $this->assertTrue( $updater->isUpdatePrepared() );
444 $this->assertTrue( $updater->isContentPrepared() );
445
446 // ParserOutput objects should have been flushed.
447 $this->assertNotSame( $mainOutput, $updater->getSlotParserOutput( SlotRecord::MAIN ) );
448 $this->assertNotSame( $canonicalOutput, $updater->getCanonicalParserOutput() );
449
450 $html = $updater->getCanonicalParserOutput()->getText();
451 $this->assertContains( '--' . $rev->getId() . '--', $html );
452
453 // TODO: MCR: ensure that when the main slot uses {{REVISIONID}} but another slot is
454 // updated, the main slot is still re-rendered!
455 }
456
457 // TODO: test failure of prepareUpdate() when called again with a different revision
458 // TODO: test failure of prepareUpdate() on inconsistency with prepareContent.
459
464 $user = $this->getTestUser()->getUser();
465
466 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
467 $update = new RevisionSlotsUpdate();
468 $update->modifyContent( SlotRecord::MAIN, $mainContent );
469
470 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
471 $updater->prepareContent( $user, $update, false );
472
473 $canonicalOutput = $updater->getCanonicalParserOutput();
474
475 $preparedEdit = $updater->getPreparedEdit();
476 $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
477 $this->assertSame( $canonicalOutput, $preparedEdit->output );
478 $this->assertSame( $mainContent, $preparedEdit->newContent );
479 $this->assertSame( $updater->getRawContent( SlotRecord::MAIN ), $preparedEdit->pstContent );
480 $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
481 $this->assertSame( null, $preparedEdit->revid );
482 }
483
488 $clock = MWTimestamp::convert( TS_UNIX, '20100101000000' );
489 MWTimestamp::setFakeTime( function () use ( &$clock ) {
490 return $clock++;
491 } );
492
493 $page = $this->getPage( __METHOD__ );
494
495 $mainContent = new WikitextContent( 'first [[main]] ~~~' );
496 $update = new MutableRevisionSlots();
497 $update->setContent( SlotRecord::MAIN, $mainContent );
498
499 $rev = $this->createRevision( $page, __METHOD__ );
500
501 $updater = $this->getDerivedPageDataUpdater( $page );
502 $updater->prepareUpdate( $rev );
503
504 $canonicalOutput = $updater->getCanonicalParserOutput();
505
506 $preparedEdit = $updater->getPreparedEdit();
507 $this->assertSame( $canonicalOutput->getCacheTime(), $preparedEdit->timestamp );
508 $this->assertSame( $canonicalOutput, $preparedEdit->output );
509 $this->assertSame( $updater->getRawContent( SlotRecord::MAIN ), $preparedEdit->pstContent );
510 $this->assertSame( $updater->getCanonicalParserOptions(), $preparedEdit->popts );
511 $this->assertSame( $rev->getId(), $preparedEdit->revid );
512 }
513
515 $user = $this->getTestUser()->getUser();
516 $page = $this->getPage( __METHOD__ );
517 $this->createRevision( $page, __METHOD__ );
518
519 $mainContent1 = new WikitextContent( 'first' );
520
521 $update = new RevisionSlotsUpdate();
522 $update->modifyContent( SlotRecord::MAIN, $mainContent1 );
523 $updater = $this->getDerivedPageDataUpdater( $page );
524 $updater->prepareContent( $user, $update, false );
525
526 $dataUpdates = $updater->getSecondaryDataUpdates();
527
528 $this->assertNotEmpty( $dataUpdates );
529
530 $linksUpdates = array_filter( $dataUpdates, function ( $du ) {
531 return $du instanceof LinksUpdate;
532 } );
533 $this->assertCount( 1, $linksUpdates );
534 }
535
541 private function defineMockContentModelForUpdateTesting( $name ) {
543 $handler = $this->getMockBuilder( TextContentHandler::class )
544 ->setConstructorArgs( [ $name ] )
545 ->setMethods(
546 [ 'getSecondaryDataUpdates', 'getDeletionUpdates', 'unserializeContent' ]
547 )
548 ->getMock();
549
550 $dataUpdate = new MWCallableUpdate( 'time' );
551 $dataUpdate->_name = "$name data update";
552
553 $deletionUpdate = new MWCallableUpdate( 'time' );
554 $deletionUpdate->_name = "$name deletion update";
555
556 $handler->method( 'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] );
557 $handler->method( 'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] );
558 $handler->method( 'unserializeContent' )->willReturnCallback(
559 function ( $text ) use ( $handler ) {
560 return $this->createMockContent( $handler, $text );
561 }
562 );
563
565 'wgContentHandlers', [
566 $name => function () use ( $handler ){
567 return $handler;
568 }
569 ]
570 );
571
572 return $handler;
573 }
574
581 private function createMockContent( ContentHandler $handler, $text ) {
583 $content = $this->getMockBuilder( TextContent::class )
584 ->setConstructorArgs( [ $text ] )
585 ->setMethods( [ 'getModel', 'getContentHandler' ] )
586 ->getMock();
587
588 $content->method( 'getModel' )->willReturn( $handler->getModelID() );
589 $content->method( 'getContentHandler' )->willReturn( $handler );
590
591 return $content;
592 }
593
595 if ( !$this->hasMultiSlotSupport() ) {
596 $this->markTestSkipped( 'Slot removal cannot happen with MCR being enabled' );
597 }
598
599 $m1 = $this->defineMockContentModelForUpdateTesting( 'M1' );
600 $a1 = $this->defineMockContentModelForUpdateTesting( 'A1' );
601 $m2 = $this->defineMockContentModelForUpdateTesting( 'M2' );
602
603 MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
604 'aux',
605 $a1->getModelID()
606 );
607
608 $mainContent1 = $this->createMockContent( $m1, 'main 1' );
609 $auxContent1 = $this->createMockContent( $a1, 'aux 1' );
610 $mainContent2 = $this->createMockContent( $m2, 'main 2' );
611
612 $user = $this->getTestUser()->getUser();
613 $page = $this->getPage( __METHOD__ );
614 $this->createRevision(
615 $page,
616 __METHOD__,
617 [ 'main' => $mainContent1, 'aux' => $auxContent1 ]
618 );
619
620 $update = new RevisionSlotsUpdate();
621 $update->modifyContent( SlotRecord::MAIN, $mainContent2 );
622 $update->removeSlot( 'aux' );
623
624 $page = $this->getPage( __METHOD__ );
625 $updater = $this->getDerivedPageDataUpdater( $page );
626 $updater->prepareContent( $user, $update, false );
627
628 $dataUpdates = $updater->getSecondaryDataUpdates();
629
630 $this->assertNotEmpty( $dataUpdates );
631
632 $updateNames = array_map( function ( $du ) {
633 return isset( $du->_name ) ? $du->_name : get_class( $du );
634 }, $dataUpdates );
635
636 $this->assertContains( LinksUpdate::class, $updateNames );
637 $this->assertContains( 'A1 deletion update', $updateNames );
638 $this->assertContains( 'M2 data update', $updateNames );
639 $this->assertNotContains( 'M1 data update', $updateNames );
640 }
641
654 private function makeRevision(
655 Title $title,
656 RevisionSlotsUpdate $update,
657 User $user,
658 $comment,
659 $id = 0,
660 $parentId = 0
661 ) {
663
664 $rev->applyUpdate( $update );
665 $rev->setUser( $user );
666 $rev->setComment( CommentStoreComment::newUnsavedComment( $comment ) );
667 $rev->setPageId( $title->getArticleID() );
668 $rev->setParentId( $parentId );
669
670 if ( $id ) {
671 $rev->setId( $id );
672 }
673
674 return $rev;
675 }
676
681 private function getMockTitle( $id = 23 ) {
682 $mock = $this->getMockBuilder( Title::class )
683 ->disableOriginalConstructor()
684 ->getMock();
685 $mock->expects( $this->any() )
686 ->method( 'getDBkey' )
687 ->will( $this->returnValue( __CLASS__ ) );
688 $mock->expects( $this->any() )
689 ->method( 'getArticleID' )
690 ->will( $this->returnValue( $id ) );
691
692 return $mock;
693 }
694
695 public function provideIsReusableFor() {
696 $title = $this->getMockTitle();
697
698 $user1 = User::newFromName( 'Alice' );
699 $user2 = User::newFromName( 'Bob' );
700
701 $content1 = new WikitextContent( 'one' );
702 $content2 = new WikitextContent( 'two' );
703
704 $update1 = new RevisionSlotsUpdate();
705 $update1->modifyContent( SlotRecord::MAIN, $content1 );
706
707 $update1b = new RevisionSlotsUpdate();
708 $update1b->modifyContent( 'xyz', $content1 );
709
710 $update2 = new RevisionSlotsUpdate();
711 $update2->modifyContent( SlotRecord::MAIN, $content2 );
712
713 $rev1 = $this->makeRevision( $title, $update1, $user1, 'rev1', 11 );
714 $rev1b = $this->makeRevision( $title, $update1b, $user1, 'rev1', 11 );
715
716 $rev2 = $this->makeRevision( $title, $update2, $user1, 'rev2', 12 );
717 $rev2x = $this->makeRevision( $title, $update2, $user2, 'rev2', 12 );
718 $rev2y = $this->makeRevision( $title, $update2, $user1, 'rev2', 122 );
719
720 yield 'any' => [
721 '$prepUser' => null,
722 '$prepRevision' => null,
723 '$prepUpdate' => null,
724 '$forUser' => null,
725 '$forRevision' => null,
726 '$forUpdate' => null,
727 '$forParent' => null,
728 '$isReusable' => true,
729 ];
730 yield 'for any' => [
731 '$prepUser' => $user1,
732 '$prepRevision' => $rev1,
733 '$prepUpdate' => $update1,
734 '$forUser' => null,
735 '$forRevision' => null,
736 '$forUpdate' => null,
737 '$forParent' => null,
738 '$isReusable' => true,
739 ];
740 yield 'unprepared' => [
741 '$prepUser' => null,
742 '$prepRevision' => null,
743 '$prepUpdate' => null,
744 '$forUser' => $user1,
745 '$forRevision' => $rev1,
746 '$forUpdate' => $update1,
747 '$forParent' => 0,
748 '$isReusable' => true,
749 ];
750 yield 'match prepareContent' => [
751 '$prepUser' => $user1,
752 '$prepRevision' => null,
753 '$prepUpdate' => $update1,
754 '$forUser' => $user1,
755 '$forRevision' => null,
756 '$forUpdate' => $update1,
757 '$forParent' => 0,
758 '$isReusable' => true,
759 ];
760 yield 'match prepareUpdate' => [
761 '$prepUser' => null,
762 '$prepRevision' => $rev1,
763 '$prepUpdate' => null,
764 '$forUser' => $user1,
765 '$forRevision' => $rev1,
766 '$forUpdate' => null,
767 '$forParent' => 0,
768 '$isReusable' => true,
769 ];
770 yield 'match all' => [
771 '$prepUser' => $user1,
772 '$prepRevision' => $rev1,
773 '$prepUpdate' => $update1,
774 '$forUser' => $user1,
775 '$forRevision' => $rev1,
776 '$forUpdate' => $update1,
777 '$forParent' => 0,
778 '$isReusable' => true,
779 ];
780 yield 'mismatch prepareContent update' => [
781 '$prepUser' => $user1,
782 '$prepRevision' => null,
783 '$prepUpdate' => $update1,
784 '$forUser' => $user1,
785 '$forRevision' => null,
786 '$forUpdate' => $update1b,
787 '$forParent' => 0,
788 '$isReusable' => false,
789 ];
790 yield 'mismatch prepareContent user' => [
791 '$prepUser' => $user1,
792 '$prepRevision' => null,
793 '$prepUpdate' => $update1,
794 '$forUser' => $user2,
795 '$forRevision' => null,
796 '$forUpdate' => $update1,
797 '$forParent' => 0,
798 '$isReusable' => false,
799 ];
800 yield 'mismatch prepareContent parent' => [
801 '$prepUser' => $user1,
802 '$prepRevision' => null,
803 '$prepUpdate' => $update1,
804 '$forUser' => $user1,
805 '$forRevision' => null,
806 '$forUpdate' => $update1,
807 '$forParent' => 7,
808 '$isReusable' => false,
809 ];
810 yield 'mismatch prepareUpdate revision update' => [
811 '$prepUser' => null,
812 '$prepRevision' => $rev1,
813 '$prepUpdate' => null,
814 '$forUser' => null,
815 '$forRevision' => $rev1b,
816 '$forUpdate' => null,
817 '$forParent' => 0,
818 '$isReusable' => false,
819 ];
820 yield 'mismatch prepareUpdate revision id' => [
821 '$prepUser' => null,
822 '$prepRevision' => $rev2,
823 '$prepUpdate' => null,
824 '$forUser' => null,
825 '$forRevision' => $rev2y,
826 '$forUpdate' => null,
827 '$forParent' => 0,
828 '$isReusable' => false,
829 ];
830 }
831
845 public function testIsReusableFor(
846 User $prepUser = null,
847 RevisionRecord $prepRevision = null,
848 RevisionSlotsUpdate $prepUpdate = null,
849 User $forUser = null,
850 RevisionRecord $forRevision = null,
851 RevisionSlotsUpdate $forUpdate = null,
852 $forParent = null,
853 $isReusable = null
854 ) {
855 $updater = $this->getDerivedPageDataUpdater( __METHOD__ );
856
857 if ( $prepUpdate ) {
858 $updater->prepareContent( $prepUser, $prepUpdate, false );
859 }
860
861 if ( $prepRevision ) {
862 $updater->prepareUpdate( $prepRevision );
863 }
864
865 $this->assertSame(
866 $isReusable,
867 $updater->isReusableFor( $forUser, $forRevision, $forUpdate, $forParent )
868 );
869 }
870
876 public function testDoUpdates() {
877 $page = $this->getPage( __METHOD__ );
878
879 $content = [ 'main' => new WikitextContent( 'first [[main]]' ) ];
880
881 if ( $this->hasMultiSlotSupport() ) {
882 $content['aux'] = new WikitextContent( 'Aux [[Nix]]' );
883
884 MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
885 'aux',
887 );
888 }
889
890 $rev = $this->createRevision( $page, 'first', $content );
891 $pageId = $page->getId();
892
893 $oldStats = $this->db->selectRow( 'site_stats', '*', '1=1' );
894 $this->db->delete( 'pagelinks', '*' );
895
896 $pcache = MediaWikiServices::getInstance()->getParserCache();
897 $pcache->deleteOptionsKey( $page );
898
899 $updater = $this->getDerivedPageDataUpdater( $page, $rev );
900 $updater->setArticleCountMethod( 'link' );
901
902 $options = []; // TODO: test *all* the options...
903 $updater->prepareUpdate( $rev, $options );
904
905 $updater->doUpdates();
906
907 // links table update
908 $pageLinks = $this->db->select(
909 'pagelinks',
910 '*',
911 [ 'pl_from' => $pageId ],
912 __METHOD__,
913 [ 'ORDER BY' => 'pl_namespace, pl_title' ]
914 );
915
916 $pageLinksRow = $pageLinks->fetchObject();
917 $this->assertInternalType( 'object', $pageLinksRow );
918 $this->assertSame( 'Main', $pageLinksRow->pl_title );
919
920 if ( $this->hasMultiSlotSupport() ) {
921 $pageLinksRow = $pageLinks->fetchObject();
922 $this->assertInternalType( 'object', $pageLinksRow );
923 $this->assertSame( 'Nix', $pageLinksRow->pl_title );
924 }
925
926 // parser cache update
927 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions() );
928 $this->assertInternalType( 'object', $cached );
929 $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
930
931 // site stats
932 $stats = $this->db->selectRow( 'site_stats', '*', '1=1' );
933 $this->assertSame( $oldStats->ss_total_pages + 1, (int)$stats->ss_total_pages );
934 $this->assertSame( $oldStats->ss_total_edits + 1, (int)$stats->ss_total_edits );
935 $this->assertSame( $oldStats->ss_good_articles + 1, (int)$stats->ss_good_articles );
936
937 // TODO: MCR: test data updates for additional slots!
938 // TODO: test update for edit without page creation
939 // TODO: test message cache purge
940 // TODO: test module cache purge
941 // TODO: test CDN purge
942 // TODO: test newtalk update
943 // TODO: test search update
944 // TODO: test site stats good_articles while turning the page into (or back from) a redir.
945 // TODO: test category membership update (with setRcWatchCategoryMembership())
946 }
947
951 public function testDoParserCacheUpdate() {
952 if ( $this->hasMultiSlotSupport() ) {
953 MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
954 'aux',
956 );
957 }
958
959 $page = $this->getPage( __METHOD__ );
960 $this->createRevision( $page, 'Dummy' );
961
962 $user = $this->getTestUser()->getUser();
963
964 $update = new RevisionSlotsUpdate();
965 $update->modifyContent( 'main', new WikitextContent( 'first [[Main]]' ) );
966
967 if ( $this->hasMultiSlotSupport() ) {
968 $update->modifyContent( 'aux', new WikitextContent( 'Aux [[Nix]]' ) );
969 }
970
971 // Emulate update after edit ----------
972 $pcache = MediaWikiServices::getInstance()->getParserCache();
973 $pcache->deleteOptionsKey( $page );
974
975 $rev = $this->makeRevision( $page->getTitle(), $update, $user, 'rev', null );
976 $rev->setTimestamp( '20100101000000' );
977 $rev->setParentId( $page->getLatest() );
978
979 $updater = $this->getDerivedPageDataUpdater( $page );
980 $updater->prepareContent( $user, $update, false );
981
982 $rev->setId( 11 );
983 $updater->prepareUpdate( $rev );
984
985 // Force the page timestamp, so we notice whether ParserOutput::getTimestamp
986 // or ParserOutput::getCacheTime are used.
987 $page->setTimestamp( $rev->getTimestamp() );
988 $updater->doParserCacheUpdate();
989
990 // The cached ParserOutput should not use the revision timestamp
991 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions(), true );
992 $this->assertInternalType( 'object', $cached );
993 $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
994
995 $this->assertSame( $rev->getTimestamp(), $cached->getCacheTime() );
996 $this->assertSame( $rev->getId(), $cached->getCacheRevisionId() );
997
998 // Emulate forced update of an old revision ----------
999 $pcache->deleteOptionsKey( $page );
1000
1001 $updater = $this->getDerivedPageDataUpdater( $page );
1002 $updater->prepareUpdate( $rev );
1003
1004 // Force the page timestamp, so we notice whether ParserOutput::getTimestamp
1005 // or ParserOutput::getCacheTime are used.
1006 $page->setTimestamp( $rev->getTimestamp() );
1007 $updater->doParserCacheUpdate();
1008
1009 // The cached ParserOutput should not use the revision timestamp
1010 $cached = $pcache->get( $page, $updater->getCanonicalParserOptions(), true );
1011 $this->assertInternalType( 'object', $cached );
1012 $this->assertSame( $updater->getCanonicalParserOutput(), $cached );
1013
1014 $this->assertGreaterThan( $rev->getTimestamp(), $cached->getCacheTime() );
1015 $this->assertSame( $rev->getId(), $cached->getCacheRevisionId() );
1016 }
1017
1027
1028}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
int $wgMultiContentRevisionSchemaMigrationStage
RevisionStore table schema migration stage (content, slots, content_models & slot_roles tables).
CommentStoreComment represents a comment stored by CommentStore.
A content handler knows how do deal with a specific type of content on a wiki page.
Class the manages updates of *_link tables as well as similar extension-managed tables.
Deferrable Update for closure/callback.
Library for creating and parsing MW-style timestamps.
getDefaultWikitextNS()
Returns the ID of a namespace that defaults to Wikitext.
mergeMwGlobalArrayValue( $name, $values)
Merges the given values into a MW global array variable.
static getTestUser( $groups=[])
Convenience method for getting an immutable test user.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.
Mutable RevisionRecord implementation, for building new revision entries programmatically.
Mutable version of RevisionSlots, for constructing a new revision.
Page revision base class.
Value object representing a content slot associated with a page revision.
A handle for managing updates for derived page data on edit, import, purge, etc.
Value object representing a modification of revision slots.
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()
testDoUpdates()
\MediaWiki\Storage\DerivedPageDataUpdater::doUpdates() \MediaWiki\Storage\DerivedPageDataUpdater::doS...
testGrabCurrentRevision()
\MediaWiki\Storage\DerivedPageDataUpdater::grabCurrentRevision() \MediaWiki\Storage\DerivedPageDataUp...
makeRevision(Title $title, RevisionSlotsUpdate $update, User $user, $comment, $id=0, $parentId=0)
Creates a dummy revision object without touching the database.
testPrepareUpdateReusesParserOutput()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate()
testPrepareUpdate()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate() \MediaWiki\Storage\DerivedPageDataUpdater:...
testGetPreparedEditAfterPrepareUpdate()
\MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
testPrepareContentInherit()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareContent() \MediaWiki\Storage\DerivedPageDataUpdater...
createRevision(WikiPage $page, $summary, $content=null)
Creates a revision in the database.
testPrepareContent()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareContent() \MediaWiki\Storage\DerivedPageDataUpdater...
testPrepareUpdateOutputReset()
\MediaWiki\Storage\DerivedPageDataUpdater::prepareUpdate() \MediaWiki\Storage\DerivedPageDataUpdater:...
testGetPreparedEditAfterPrepareContent()
\MediaWiki\Storage\DerivedPageDataUpdater::getPreparedEdit()
testDoParserCacheUpdate()
\MediaWiki\Storage\DerivedPageDataUpdater::doParserCacheUpdate()
testGetCanonicalParserOptions()
\MediaWiki\Storage\DerivedPageDataUpdater::getCanonicalParserOptions()
Base content handler implementation for flat text contents.
Content object implementation for representing flat text.
Represents a title within MediaWiki.
Definition Title.php:40
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:48
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:585
Class representing a MediaWiki article and history.
Definition WikiPage.php:45
newPageUpdater(User $user, RevisionSlotsUpdate $forUpdate=null)
Returns a PageUpdater for creating new revisions on this page (or creating the page).
Content object for wiki text pages.
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
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:296
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:244
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:295
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:894
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:1999
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:955
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:2011
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:271
return true to allow those checks to and false if checking is done & $user
Definition hooks.txt:1510
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:1779
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:37
Base interface for content objects.
Definition Content.php:34
$content
$page->newPageUpdater($user) $updater