3 use MediaWiki\Category\Category
;
4 use MediaWiki\CommentStore\CommentStoreComment
;
5 use MediaWiki\Content\Content
;
6 use MediaWiki\Content\ContentHandler
;
7 use MediaWiki\Content\Renderer\ContentRenderer
;
8 use MediaWiki\Content\TextContent
;
9 use MediaWiki\Content\WikitextContent
;
10 use MediaWiki\Deferred\SiteStatsUpdate
;
11 use MediaWiki\Edit\PreparedEdit
;
12 use MediaWiki\MainConfigNames
;
13 use MediaWiki\MediaWikiServices
;
14 use MediaWiki\Parser\ParserOptions
;
15 use MediaWiki\Parser\ParserOutput
;
16 use MediaWiki\Permissions\Authority
;
17 use MediaWiki\Revision\MutableRevisionRecord
;
18 use MediaWiki\Revision\RevisionRecord
;
19 use MediaWiki\Revision\SlotRecord
;
20 use MediaWiki\Storage\RevisionSlotsUpdate
;
21 use MediaWiki\Tests\Unit\DummyServicesTrait
;
22 use MediaWiki\Tests\Unit\Permissions\MockAuthorityTrait
;
23 use MediaWiki\Tests\User\TempUser\TempUserTestTrait
;
24 use MediaWiki\Title\Title
;
25 use MediaWiki\User\User
;
26 use MediaWiki\Utils\MWTimestamp
;
27 use PHPUnit\Framework\Assert
;
28 use Wikimedia\Rdbms\IDBAccessObject
;
29 use Wikimedia\TestingAccessWrapper
;
35 class WikiPageDbTest
extends MediaWikiLangTestCase
{
36 use DummyServicesTrait
;
37 use MockAuthorityTrait
;
38 use TempUserTestTrait
;
40 protected function tearDown(): void
{
41 ParserOptions
::clearStaticCache();
46 * @param Title|string $title
47 * @param string|null $model
50 private function newPage( $title, $model = null ) {
51 if ( is_string( $title ) ) {
52 $ns = $this->getDefaultWikitextNS();
53 $title = Title
::newFromText( $title, $ns );
56 return new WikiPage( $title );
60 * @param string|Title|WikiPage $page
61 * @param string|Content|Content[] $content
62 * @param int|null $model
63 * @param Authority|null $performer
67 protected function createPage( $page, $content, $model = null, ?Authority
$performer = null ) {
68 if ( is_string( $page ) ||
$page instanceof Title
) {
69 $page = $this->newPage( $page, $model );
72 $performer ??
= $this->getTestUser()->getUser();
74 if ( is_string( $content ) ) {
75 $content = ContentHandler
::makeContent( $content, $page->getTitle(), $model );
78 if ( !is_array( $content ) ) {
79 $content = [ SlotRecord
::MAIN
=> $content ];
82 $updater = $page->newPageUpdater( $performer );
84 foreach ( $content as $role => $cnt ) {
85 $updater->setContent( $role, $cnt );
88 $updater->saveRevision( CommentStoreComment
::newUnsavedComment( "testing" ) );
89 if ( !$updater->wasSuccessful() ) {
90 $this->fail( $updater->getStatus()->getWikiText() );
96 public function testSerialization_fails() {
97 $this->expectException( LogicException
::class );
98 $page = $this->createPage( __METHOD__
, __METHOD__
);
102 public static function provideTitlesThatCannotExist() {
103 yield
'Special' => [ NS_SPECIAL
, 'Recentchanges' ]; // existing special page
104 yield
'Invalid character' => [ NS_MAIN
, '#' ]; // bad character
108 * @dataProvider provideTitlesThatCannotExist
110 public function testConstructionWithPageThatCannotExist( $ns, $text ) {
111 $title = Title
::makeTitle( $ns, $text );
112 $this->expectException( InvalidArgumentException
::class );
113 new WikiPage( $title );
116 public function testPrepareContentForEdit() {
117 $performer = $this->mockUserAuthorityWithPermissions(
118 $this->getTestUser()->getUserIdentity(),
121 $sysop = $this->getTestUser( [ 'sysop' ] )->getUserIdentity();
123 $page = $this->createPage( __METHOD__
, __METHOD__
, null, $performer );
124 $title = $page->getTitle();
126 $content = ContentHandler
::makeContent(
127 "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
128 . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
130 CONTENT_MODEL_WIKITEXT
132 $content2 = ContentHandler
::makeContent(
133 "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
134 . "Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
136 CONTENT_MODEL_WIKITEXT
139 $edit = $page->prepareContentForEdit( $content, null, $performer->getUser(), null, false );
141 $this->assertInstanceOf(
142 ParserOptions
::class,
146 $this->assertStringContainsString( '</a>', $edit->output
->getRawText(), "output" );
147 $this->assertStringContainsString(
148 'consetetur sadipscing elitr',
149 $edit->output
->getRawText(), "output"
152 $this->assertTrue( $content->equals( $edit->newContent
), "newContent field" );
153 $this->assertTrue( $content->equals( $edit->pstContent
), "pstContent field" );
154 $this->assertSame( $edit->output
, $edit->output
, "output field" );
155 $this->assertSame( $edit->popts
, $edit->popts
, "popts field" );
156 $this->assertSame( null, $edit->revid
, "revid field" );
158 // PreparedUpdate matches PreparedEdit
159 $update = $page->getCurrentUpdate();
160 $this->assertSame( $edit->output
, $update->getCanonicalParserOutput() );
162 // Re-using the prepared info if possible
163 $sameEdit = $page->prepareContentForEdit( $content, null, $performer->getUser(), null, false );
164 $this->assertPreparedEditEquals( $edit, $sameEdit, 'equivalent PreparedEdit' );
165 $this->assertSame( $edit->pstContent
, $sameEdit->pstContent
, 're-use output' );
166 $this->assertSame( $edit->output
, $sameEdit->output
, 're-use output' );
168 // re-using the same PreparedUpdate
169 $this->assertSame( $update, $page->getCurrentUpdate() );
171 // Not re-using the same PreparedEdit if not possible
172 $edit2 = $page->prepareContentForEdit( $content2, null, $performer->getUser(), null, false );
173 $this->assertPreparedEditNotEquals( $edit, $edit2 );
174 $this->assertStringContainsString( 'At vero eos', $edit2->pstContent
->serialize(), "content" );
176 // Not re-using the same PreparedUpdate
177 $this->assertNotSame( $update, $page->getCurrentUpdate() );
179 // Check pre-safe transform
180 $this->assertStringContainsString( '[[gubergren]]', $edit2->pstContent
->serialize() );
181 $this->assertStringNotContainsString( '~~~~', $edit2->pstContent
->serialize() );
183 $edit3 = $page->prepareContentForEdit( $content2, null, $sysop, null, false );
184 $this->assertPreparedEditNotEquals( $edit2, $edit3 );
186 // TODO: test with passing revision, then same without revision.
189 public function testDoEditUpdates() {
190 $this->hideDeprecated( 'WikiPage::doEditUpdates' );
191 $user = $this->getTestUser()->getUserIdentity();
193 // NOTE: if site stats get out of whack and drop below 0,
194 // that causes a DB error during tear-down. So bump the
195 // numbers high enough to not drop below 0.
196 $siteStatsUpdate = SiteStatsUpdate
::factory(
197 [ 'edits' => 1000, 'articles' => 1000, 'pages' => 1000 ]
199 $siteStatsUpdate->doUpdate();
201 $page = $this->createPage( __METHOD__
, __METHOD__
);
203 $comment = CommentStoreComment
::newUnsavedComment( __METHOD__
);
205 // PST turns [[|foo]] into [[foo]]
206 $content = $this->getServiceContainer()
207 ->getContentHandlerFactory()
208 ->getContentHandler( CONTENT_MODEL_WIKITEXT
)
209 ->unserializeContent( __METHOD__
. ' [[|foo]][[bar]]' );
211 $revRecord = new MutableRevisionRecord( $page->getTitle() );
212 $revRecord->setContent( SlotRecord
::MAIN
, $content );
213 $revRecord->setUser( $user );
214 $revRecord->setTimestamp( '20170707040404' );
215 $revRecord->setPageId( $page->getId() );
216 $revRecord->setId( 9989 );
217 $revRecord->setMinorEdit( true );
218 $revRecord->setComment( $comment );
220 $page->doEditUpdates( $revRecord, $user );
222 // TODO: test various options; needs temporary hooks
224 $res = $this->getDb()->newSelectQueryBuilder()
226 ->from( 'pagelinks' )
227 ->where( [ 'pl_from' => $page->getId() ] )
229 $n = $res->numRows();
232 $this->assertSame( 1, $n, 'pagelinks should contain only one link if PST was not applied' );
235 public function testDoUserEditContent() {
236 $this->overrideConfigValue( MainConfigNames
::PageCreationLog
, true );
238 $page = $this->newPage( __METHOD__
);
239 $title = $page->getTitle();
241 $user1 = $this->getTestUser()->getUser();
242 // Use the confirmed group for user2 to make sure the user is different
243 $user2 = $this->getTestUser( [ 'confirmed' ] )->getUser();
245 $content = ContentHandler
::makeContent(
246 "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
247 . " nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
249 CONTENT_MODEL_WIKITEXT
252 $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user1 );
254 $status = $page->doUserEditContent( $content, $user1, "[[testing]] 1", EDIT_NEW
);
256 $this->assertStatusGood( $status );
257 $this->assertTrue( $status->value
['new'], 'new' );
258 $this->assertNotNull( $status->getNewRevision(), 'revision-record' );
260 $statusRevRecord = $status->getNewRevision();
261 $this->assertSame( $statusRevRecord->getId(), $page->getRevisionRecord()->getId() );
262 $this->assertSame( $statusRevRecord->getSha1(), $page->getRevisionRecord()->getSha1() );
264 $statusRevRecord->getContent( SlotRecord
::MAIN
)->equals( $content ),
268 $revRecord = $page->getRevisionRecord();
269 $recentChange = $this->getServiceContainer()
271 ->getRecentChange( $revRecord );
272 $preparedEditAfter = $page->prepareContentForEdit( $content, $revRecord, $user1 );
274 $this->assertNotNull( $recentChange );
275 $this->assertSame( $revRecord->getId(), (int)$recentChange->getAttribute( 'rc_this_oldid' ) );
277 // make sure that cached ParserOutput gets re-used throughout
278 $this->assertSame( $preparedEditBefore->output
, $preparedEditAfter->output
);
280 $id = $page->getId();
282 // Test page creation logging
283 $this->newSelectQueryBuilder()
284 ->select( [ 'log_type', 'log_action' ] )
286 ->where( [ 'log_page' => $id ] )
287 ->assertResultSet( [ [ 'create', 'create' ] ] );
289 $this->assertGreaterThan( 0, $title->getArticleID(), "Title object should have new page id" );
290 $this->assertGreaterThan( 0, $id, "WikiPage should have new page id" );
291 $this->assertTrue( $title->exists(), "Title object should indicate that the page now exists" );
292 $this->assertTrue( $page->exists(), "WikiPage object should indicate that the page now exists" );
294 # ------------------------
295 $res = $this->getDb()->newSelectQueryBuilder()
297 ->from( 'pagelinks' )
298 ->where( [ 'pl_from' => $id ] )
300 $n = $res->numRows();
303 $this->assertSame( 1, $n, 'pagelinks should contain one link from the page' );
305 # ------------------------
306 $page = new WikiPage( $title );
308 $retrieved = $page->getContent();
309 $this->assertTrue( $content->equals( $retrieved ), 'retrieved content doesn\'t equal original' );
311 # ------------------------
312 $page = new WikiPage( $title );
314 // try null edit, with a different user
315 $status = $page->doUserEditContent( $content, $user2, 'This changes nothing', EDIT_UPDATE
, false );
316 $this->assertStatusWarning( 'edit-no-change', $status );
317 $this->assertFalse( $status->value
['new'], 'new' );
318 $this->assertNull( $status->getNewRevision(), 'revision-record' );
319 $this->assertNotNull( $page->getRevisionRecord() );
321 $page->getRevisionRecord()->getContent( SlotRecord
::MAIN
)->equals( $content ),
325 # ------------------------
326 $content = ContentHandler
::makeContent(
327 "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
328 . "Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
330 CONTENT_MODEL_WIKITEXT
333 $status = $page->doUserEditContent( $content, $user1, "testing 2", EDIT_UPDATE
);
334 $this->assertStatusGood( $status );
335 $this->assertFalse( $status->value
['new'], 'new' );
336 $this->assertNotNull( $status->getNewRevision(), 'revision-record' );
337 $statusRevRecord = $status->getNewRevision();
338 $this->assertSame( $statusRevRecord->getId(), $page->getRevisionRecord()->getId() );
339 $this->assertSame( $statusRevRecord->getSha1(), $page->getRevisionRecord()->getSha1() );
341 $statusRevRecord->getContent( SlotRecord
::MAIN
)->equals( $content ),
342 'not equals (PST must substitute signature)'
345 $revRecord = $page->getRevisionRecord();
346 $recentChange = $this->getServiceContainer()
348 ->getRecentChange( $revRecord );
349 $this->assertNotNull( $recentChange );
350 $this->assertSame( $revRecord->getId(), (int)$recentChange->getAttribute( 'rc_this_oldid' ) );
352 # ------------------------
353 $page = new WikiPage( $title );
355 $retrieved = $page->getContent();
356 $newText = $retrieved->serialize();
357 $this->assertStringContainsString( '[[gubergren]]', $newText, 'New text must replace old text.' );
358 $this->assertStringNotContainsString( '~~~~', $newText, 'PST must substitute signature.' );
360 # ------------------------
361 $res = $this->getDb()->newSelectQueryBuilder()
363 ->from( 'pagelinks' )
364 ->where( [ 'pl_from' => $id ] )
366 $n = $res->numRows();
369 // two in page text and two in signature
370 $this->assertEquals( 4, $n, 'pagelinks should contain four links from the page' );
373 public static function provideNonPageTitle() {
374 yield
'bad case and char' => [ Title
::makeTitle( NS_MAIN
, 'lower case and bad # char' ) ];
375 yield
'empty' => [ Title
::makeTitle( NS_MAIN
, '' ) ];
376 yield
'special' => [ Title
::makeTitle( NS_SPECIAL
, 'Dummy' ) ];
377 yield
'relative' => [ Title
::makeTitle( NS_MAIN
, '', '#section' ) ];
378 yield
'interwiki' => [ Title
::makeTitle( NS_MAIN
, 'Foo', '', 'acme' ) ];
382 * @dataProvider provideNonPageTitle
384 public function testDoUserEditContent_bad_page( $title ) {
385 $user1 = $this->getTestUser()->getUser();
387 $content = ContentHandler
::makeContent(
390 CONTENT_MODEL_WIKITEXT
393 $this->filterDeprecated( '/WikiPage constructed on a Title that cannot exist as a page/' );
395 $page = $this->newPage( $title );
396 $page->doUserEditContent( $content, $user1, "[[testing]] 1", EDIT_NEW
);
397 } catch ( Exception
$ex ) {
398 // Throwing is an acceptable way to react to an invalid title,
399 // as long as no garbage is written to the database.
402 $row = $this->getDb()->newSelectQueryBuilder()
405 ->where( [ 'page_namespace' => $title->getNamespace(), 'page_title' => $title->getDBkey() ] )
408 $this->assertFalse( $row );
411 public function testDoUserEditContent_twice() {
412 $title = Title
::newFromText( __METHOD__
);
413 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
414 $content = ContentHandler
::makeContent( '$1 van $2', $title );
416 $user = $this->getTestUser()->getUser();
418 // Make sure we can do the exact same save twice.
419 // This tests checks that internal caches are reset as appropriate.
420 $status1 = $page->doUserEditContent( $content, $user, __METHOD__
);
421 $status2 = $page->doUserEditContent( $content, $user, __METHOD__
);
423 $this->assertStatusGood( $status1 );
424 $this->assertStatusWarning( 'edit-no-change', $status2 );
426 $this->assertNotNull( $status1->getNewRevision(), 'OK' );
427 $this->assertNull( $status2->getNewRevision(), 'OK' );
431 * Undeletion is covered in PageArchiveTest::testUndeleteRevisions()
433 * TODO: Revision deletion
435 public function testDoDeleteArticleReal() {
436 $this->overrideConfigValues( [
437 MainConfigNames
::RCWatchCategoryMembership
=> false,
440 $page = $this->createPage(
442 "[[original text]] foo",
443 CONTENT_MODEL_WIKITEXT
445 $id = $page->getId();
446 $user = $this->getTestSysop()->getUser();
448 $reason = "testing deletion";
449 $status = $page->doDeleteArticleReal( $reason, $user );
452 $page->getTitle()->getArticleID() > 0,
453 "Title object should now have page id 0"
455 $this->assertSame( 0, $page->getId(), "WikiPage should now have page id 0" );
458 "WikiPage::exists should return false after page was deleted"
462 "WikiPage::getContent should return null after page was deleted"
465 $t = Title
::newFromText( $page->getTitle()->getPrefixedText() );
468 "Title::exists should return false after page was deleted"
474 # ------------------------
475 $res = $this->getDb()->newSelectQueryBuilder()
477 ->from( 'pagelinks' )
478 ->where( [ 'pl_from' => $id ] )
480 $n = $res->numRows();
483 $this->assertSame( 0, $n, 'pagelinks should contain no more links from the page' );
485 // Test deletion logging
486 $logId = $status->getValue();
487 $commentQuery = $this->getServiceContainer()->getCommentStore()->getJoin( 'log_comment' );
488 $this->newSelectQueryBuilder()
492 'log_comment' => $commentQuery['fields']['log_comment_text'],
498 ->tables( $commentQuery['tables'] )
499 ->where( [ 'log_id' => $logId ] )
500 ->joinConds( $commentQuery['joins'] )
505 (string)$user->getActorId(),
506 (string)$page->getTitle()->getNamespace(),
507 $page->getTitle()->getDBkey(),
512 * TODO: Test more stuff about suppression.
514 public function testDoDeleteArticleReal_suppress() {
515 $page = $this->createPage(
517 "[[original text]] foo",
518 CONTENT_MODEL_WIKITEXT
521 $user = $this->getTestSysop()->getUser();
522 $status = $page->doDeleteArticleReal(
523 /* reason */ "testing deletion",
528 // Test suppression logging
529 $logId = $status->getValue();
530 $commentQuery = $this->getServiceContainer()->getCommentStore()->getJoin( 'log_comment' );
531 $this->newSelectQueryBuilder()
535 'log_comment' => $commentQuery['fields']['log_comment_text'],
541 ->tables( $commentQuery['tables'] )
542 ->where( [ 'log_id' => $logId ] )
543 ->joinConds( $commentQuery['joins'] )
548 (string)$user->getActorId(),
549 (string)$page->getTitle()->getNamespace(),
550 $page->getTitle()->getDBkey(),
553 $lookup = $this->getServiceContainer()->getArchivedRevisionLookup();
554 $archivedRevs = $lookup->listRevisions( $page->getTitle() );
555 if ( !$archivedRevs ||
$archivedRevs->numRows() !== 1 ) {
556 $this->fail( 'Unexpected number of archived revisions' );
558 $archivedRev = $this->getServiceContainer()->getRevisionStore()
559 ->newRevisionFromArchiveRow( $archivedRevs->current() );
562 $archivedRev->getContent( SlotRecord
::MAIN
, RevisionRecord
::FOR_PUBLIC
),
563 "Archived content should be null after the page was suppressed for general users"
567 $archivedRev->getContent(
569 RevisionRecord
::FOR_THIS_USER
,
570 $this->getTestUser()->getUser()
572 "Archived content should be null after the page was suppressed for individual users"
575 $this->hideDeprecated( 'ContentHandler::getSlotDiffRendererInternal' );
577 $archivedRev->getContent( SlotRecord
::MAIN
, RevisionRecord
::FOR_THIS_USER
, $user ),
578 "Archived content should be null after the page was suppressed even for a sysop"
582 public function testGetContent() {
583 $page = $this->newPage( __METHOD__
);
585 $content = $page->getContent();
586 $this->assertNull( $content );
588 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT
);
590 $content = $page->getContent();
591 $this->assertEquals( "some text", $content->getText() );
594 public function testExists() {
595 $page = $this->newPage( __METHOD__
);
596 $this->assertFalse( $page->exists() );
598 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT
);
599 $this->assertTrue( $page->exists() );
601 $page = new WikiPage( $page->getTitle() );
602 $this->assertTrue( $page->exists() );
604 $this->deletePage( $page, "done testing" );
605 $this->assertFalse( $page->exists() );
607 $page = new WikiPage( $page->getTitle() );
608 $this->assertFalse( $page->exists() );
611 public static function provideHasViewableContent() {
613 [ 'WikiPageTest_testHasViewableContent', false, true ],
614 [ 'MediaWiki:WikiPageTest_testHasViewableContent', false ],
615 [ 'MediaWiki:help', true ],
620 * @dataProvider provideHasViewableContent
622 public function testHasViewableContent( $title, $viewable, $create = false ) {
623 $page = $this->newPage( $title );
624 $this->assertEquals( $viewable, $page->hasViewableContent() );
627 $this->createPage( $page, "some text", CONTENT_MODEL_WIKITEXT
);
628 $this->assertTrue( $page->hasViewableContent() );
630 $page = new WikiPage( $page->getTitle() );
631 $this->assertTrue( $page->hasViewableContent() );
635 public static function provideGetRedirectTarget() {
637 [ 'WikiPageTest_testGetRedirectTarget_1', CONTENT_MODEL_WIKITEXT
, "hello world", null ],
639 'WikiPageTest_testGetRedirectTarget_2',
640 CONTENT_MODEL_WIKITEXT
,
641 "#REDIRECT [[hello world]]",
644 // The below added to protect against Media namespace
645 // redirects which throw a fatal: (T203942)
647 'WikiPageTest_testGetRedirectTarget_3',
648 CONTENT_MODEL_WIKITEXT
,
649 "#REDIRECT [[Media:hello_world]]",
652 // Test fragments longer than 255 bytes (T207876)
654 'WikiPageTest_testGetRedirectTarget_4',
655 CONTENT_MODEL_WIKITEXT
,
656 '#REDIRECT [[Foobar#🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴]]',
657 'Foobar#🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴'
663 * @dataProvider provideGetRedirectTarget
665 * @covers \MediaWiki\Page\RedirectStore
667 public function testGetRedirectTarget( $title, $model, $text, $target ) {
668 $this->overrideConfigValues( [
669 MainConfigNames
::CapitalLinks
=> true,
670 // The file redirect can trigger http request with UseInstantCommons = true
671 MainConfigNames
::ForeignFileRepos
=> [],
674 $titleFormatter = $this->getServiceContainer()->getTitleFormatter();
676 $page = $this->createPage( $title, $text, $model );
678 # double check, because this test seems to fail for no reason for some people.
679 $c = $page->getContent();
680 $this->assertEquals( WikitextContent
::class, get_class( $c ) );
682 # now, test the actual redirect
683 $redirectStore = $this->getServiceContainer()->getRedirectStore();
684 $t = $redirectStore->getRedirectTarget( $page );
686 $this->assertEquals( $target, $t ?
$titleFormatter->getFullText( $t ) : null );
690 * @dataProvider provideGetRedirectTarget
692 public function testIsRedirect( $title, $model, $text, $target ) {
693 // The file redirect can trigger http request with UseInstantCommons = true
694 $this->overrideConfigValue( MainConfigNames
::ForeignFileRepos
, [] );
696 $page = $this->createPage( $title, $text, $model );
697 $this->assertEquals( $target !== null, $page->isRedirect() );
700 public static function provideIsCountable() {
704 [ 'WikiPageTest_testIsCountable',
705 CONTENT_MODEL_WIKITEXT
,
710 [ 'WikiPageTest_testIsCountable',
711 CONTENT_MODEL_WIKITEXT
,
718 [ 'WikiPageTest_testIsCountable',
719 CONTENT_MODEL_WIKITEXT
,
724 [ 'WikiPageTest_testIsCountable',
725 CONTENT_MODEL_WIKITEXT
,
732 [ 'WikiPageTest_testIsCountable',
733 CONTENT_MODEL_WIKITEXT
,
738 [ 'WikiPageTest_testIsCountable',
739 CONTENT_MODEL_WIKITEXT
,
745 // not a content namespace
746 [ 'Talk:WikiPageTest_testIsCountable',
747 CONTENT_MODEL_WIKITEXT
,
752 [ 'Talk:WikiPageTest_testIsCountable',
753 CONTENT_MODEL_WIKITEXT
,
759 // not a content namespace, different model
760 [ 'MediaWiki:WikiPageTest_testIsCountable.js',
766 [ 'MediaWiki:WikiPageTest_testIsCountable.js',
776 * @dataProvider provideIsCountable
778 public function testIsCountable( $title, $model, $text, $mode, $expected ) {
779 $this->overrideConfigValue( MainConfigNames
::ArticleCountMethod
, $mode );
781 $title = Title
::newFromText( $title );
783 $page = $this->createPage( $title, $text, $model );
785 $editInfo = $page->prepareContentForEdit(
788 $this->getTestUser()->getUser()
791 $v = $page->isCountable();
792 $w = $page->isCountable( $editInfo );
797 "isCountable( null ) returned unexpected value " . var_export( $v, true )
798 . " instead of " . var_export( $expected, true )
799 . " in mode `$mode` for text \"$text\""
805 "isCountable( \$editInfo ) returned unexpected value " . var_export( $v, true )
806 . " instead of " . var_export( $expected, true )
807 . " in mode `$mode` for text \"$text\""
812 * @dataProvider provideMakeParserOptions
814 public function testMakeParserOptions( int $ns, string $title, string $model, $context, callable
$expectation ) {
815 // Ensure we're working with the default values during this test.
816 $this->overrideConfigValues( [
817 MainConfigNames
::TextModelsToParse
=> [
818 CONTENT_MODEL_WIKITEXT
,
819 CONTENT_MODEL_JAVASCRIPT
,
822 MainConfigNames
::DisableLangConversion
=> false,
824 // Call the context function first, which lets us setup the
825 // overall wiki context before invoking the function-under-test
826 if ( is_callable( $context ) ) {
827 $context = $context( $this );
829 $page = $this->createPage(
830 Title
::makeTitle( $ns, $title ), __METHOD__
, $model
832 $parserOptions = $page->makeParserOptions( $context );
833 $expected = $expectation();
834 $this->assertTrue( $expected->matches( $parserOptions ) );
838 * @dataProvider provideMakeParserOptions
840 public function testMakeParserOptionsFromTitleAndModel( int $ns, string $title, string $model, $context, callable
$expectation ) {
841 // Ensure we're working with the default values during this test.
842 $this->overrideConfigValues( [
843 MainConfigNames
::TextModelsToParse
=> [
844 CONTENT_MODEL_WIKITEXT
,
845 CONTENT_MODEL_JAVASCRIPT
,
848 MainConfigNames
::DisableLangConversion
=> false,
850 // Call the context function first, which lets us setup the
851 // overall wiki context before invoking the function-under-test
852 if ( is_callable( $context ) ) {
853 $context = $context( $this );
855 $parserOptions = WikiPage
::makeParserOptionsFromTitleAndModel(
856 Title
::makeTitle( $ns, $title ), $model, $context
858 $expected = $expectation();
859 $this->assertTrue( $expected->matches( $parserOptions ) );
862 public static function provideMakeParserOptions() {
863 // Default canonical parser options for a normal wikitext page
865 NS_MAIN
, 'Main Page', CONTENT_MODEL_WIKITEXT
, 'canonical',
867 return ParserOptions
::newFromAnon();
870 // JavaScript should have Table Of Contents suppressed
872 NS_MAIN
, 'JavaScript Test', CONTENT_MODEL_JAVASCRIPT
, 'canonical',
874 return ParserOptions
::newFromAnon();
877 // CSS should have Table Of Contents suppressed
879 NS_MAIN
, 'CSS Test', CONTENT_MODEL_CSS
, 'canonical',
881 return ParserOptions
::newFromAnon();
884 // Language Conversion tables have content conversion disabled
886 NS_MEDIAWIKI
, 'Conversiontable/Test', CONTENT_MODEL_WIKITEXT
,
887 static function ( $test ) {
888 // Switch wiki to a language where LanguageConverter is enabled
889 $test->setContentLang( 'zh' );
890 $test->setUserLang( 'en' );
894 $po = ParserOptions
::newFromAnon();
895 $po->disableContentConversion();
896 // "Canonical" PO should use content language not user language
897 Assert
::assertSame( 'zh', $po->getUserLang() );
901 // Test "non-canonical" options: parser option should use user
902 // language here, not content language
905 NS_MAIN
, 'Main Page', CONTENT_MODEL_WIKITEXT
,
906 static function ( $test ) use ( &$user ) {
907 $test->setContentLang( 'qqx' );
908 $test->setUserLang( 'fr' );
909 $user = $test->getTestUser()->getUser();
912 static function () use ( &$user ) {
913 $po = ParserOptions
::newFromUser( $user );
914 Assert
::assertSame( 'fr', $po->getUserLang() );
920 public static function provideGetParserOutput() {
923 CONTENT_MODEL_WIKITEXT
,
925 '<div class="mw-content-ltr mw-parser-output" lang="en" dir="ltr"><p>hello <i>world</i></p></div>'
928 CONTENT_MODEL_JAVASCRIPT
,
929 "var test='<h2>not really a heading</h2>';",
930 "<pre class=\"mw-code mw-js\" dir=\"ltr\">\nvar test='<h2>not really a heading</h2>';\n</pre>",
934 "/* Not ''wikitext'' */",
935 "<pre class=\"mw-code mw-css\" dir=\"ltr\">\n/* Not ''wikitext'' */\n</pre>",
942 * @dataProvider provideGetParserOutput
944 public function testGetParserOutput( $model, $text, $expectedHtml ) {
945 $page = $this->createPage( __METHOD__
, $text, $model );
947 $opt = $page->makeParserOptions( 'canonical' );
948 $po = $page->getParserOutput( $opt );
949 $pipeline = MediaWikiServices
::getInstance()->getDefaultOutputPipeline();
950 $text = $pipeline->run( $po, $opt, [] )->getContentHolderText();
952 $text = trim( preg_replace( '/<!--.*?-->/sm', '', $text ) ); # strip injected comments
953 $text = preg_replace( '!\s*(</p>|</div>)!m', '\1', $text ); # don't let tidy confuse us
955 $this->assertEquals( $expectedHtml, $text );
958 public function testGetParserOutput_nonexisting() {
959 $page = new WikiPage( Title
::newFromText( __METHOD__
) );
961 $opt = ParserOptions
::newFromAnon();
962 $po = $page->getParserOutput( $opt );
964 $this->assertFalse( $po, "getParserOutput() shall return false for non-existing pages." );
967 public function testGetParserOutput_badrev() {
968 $page = $this->createPage( __METHOD__
, 'dummy', CONTENT_MODEL_WIKITEXT
);
970 $opt = ParserOptions
::newFromAnon();
971 $po = $page->getParserOutput( $opt, $page->getLatest() +
1234 );
973 // @todo would be neat to also test deleted revision
975 $this->assertFalse( $po, "getParserOutput() shall return false for non-existing revisions." );
978 public const SECTIONS
=
992 public function dataReplaceSection() {
993 // NOTE: assume the Help namespace to contain wikitext
995 [ 'Help:WikiPageTest_testReplaceSection',
996 CONTENT_MODEL_WIKITEXT
,
1001 trim( preg_replace( '/^Intro/m', 'No more', self
::SECTIONS
) )
1003 [ 'Help:WikiPageTest_testReplaceSection',
1004 CONTENT_MODEL_WIKITEXT
,
1011 [ 'Help:WikiPageTest_testReplaceSection',
1012 CONTENT_MODEL_WIKITEXT
,
1015 "== TEST ==\nmore fun",
1017 trim( preg_replace( '/^== test ==.*== foo ==/sm',
1018 "== TEST ==\nmore fun\n\n== foo ==",
1021 [ 'Help:WikiPageTest_testReplaceSection',
1022 CONTENT_MODEL_WIKITEXT
,
1027 trim( self
::SECTIONS
)
1029 [ 'Help:WikiPageTest_testReplaceSection',
1030 CONTENT_MODEL_WIKITEXT
,
1035 trim( self
::SECTIONS
) . "\n\n== New ==\n\nNo more"
1041 * @dataProvider dataReplaceSection
1043 public function testReplaceSectionContent( $title, $model, $text, $section,
1044 $with, $sectionTitle, $expected
1046 $page = $this->createPage( $title, $text, $model );
1048 $content = ContentHandler
::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1049 /** @var TextContent $c */
1050 $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
1052 $this->assertEquals( $expected, $c ?
trim( $c->getText() ) : null );
1056 * @dataProvider dataReplaceSection
1058 public function testReplaceSectionAtRev( $title, $model, $text, $section,
1059 $with, $sectionTitle, $expected
1061 $page = $this->createPage( $title, $text, $model );
1062 $baseRevId = $page->getLatest();
1064 $content = ContentHandler
::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1065 /** @var TextContent $c */
1066 $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId );
1068 $this->assertEquals( $expected, $c ?
trim( $c->getText() ) : null );
1071 public static function provideGetAutoDeleteReason() {
1081 [ "first edit", null ],
1083 "/first edit.*only contributor/",
1089 [ "first edit", null ],
1090 [ "second edit", null ],
1092 "/second edit.*only contributor/",
1098 [ "first edit", "127.0.2.22" ],
1099 [ "second edit", "127.0.3.33" ],
1109 . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
1110 . " nonumy eirmod tempor invidunt ut labore et dolore magna "
1111 . "aliquyam erat, sed diam voluptua. At vero eos et accusam "
1112 . "et justo duo dolores et ea rebum. Stet clita kasd gubergren, "
1113 . "no sea takimata sanctus est Lorem ipsum dolor sit amet. "
1114 . " this here is some more filler content added to try and "
1115 . "reach the maximum automatic summary length so that this is"
1116 . " truncated ipot sodit colrad ut ad olve amit basul dat"
1117 . "Dorbet romt crobit trop bri. DannyS712 put me here lor pe"
1118 . " ode quob zot bozro see also T22281 for background pol sup"
1119 . "Lorem ipsum dolor sit amet'",
1123 '/first edit:.*\.\.\."/',
1129 [ "first edit", "127.0.2.22" ],
1130 [ "", "127.0.3.33" ],
1132 "/before blanking.*first edit/",
1140 * @dataProvider provideGetAutoDeleteReason
1142 public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
1143 $this->disableAutoCreateTempUser();
1145 // NOTE: assume Help namespace to contain wikitext
1146 $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
1150 foreach ( $edits as $edit ) {
1153 if ( !empty( $edit[1] ) ) {
1154 $user->setName( $edit[1] );
1159 $content = ContentHandler
::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
1161 $page->doUserEditContent( $content, $user, "test edit $c", $c < 2 ? EDIT_NEW
: 0 );
1166 $this->hideDeprecated( 'WikiPage::getAutoDeleteReason:' );
1167 $this->hideDeprecated( 'MediaWiki\\Content\\ContentHandler::getAutoDeleteReason:' );
1168 $reason = $page->getAutoDeleteReason( $hasHistory );
1170 if ( is_bool( $expectedResult ) ||
$expectedResult === null ) {
1171 $this->assertEquals( $expectedResult, $reason );
1173 $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
1174 "Autosummary didn't match expected pattern $expectedResult: $reason" );
1177 $this->assertEquals( $expectedHistory, $hasHistory,
1178 "expected \$hasHistory to be " . var_export( $expectedHistory, true ) );
1181 public static function providePreSaveTransform() {
1183 [ 'hello this is ~~~',
1184 "hello this is [[Special:Contributions/127.0.0.1|127.0.0.1]]",
1186 [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1187 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1192 public function testLoadPageData() {
1193 $title = Title
::makeTitle( NS_MAIN
, 'SomePage' );
1194 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
1196 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_NORMAL
) );
1197 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_LATEST
) );
1198 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_LOCKING
) );
1199 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_EXCLUSIVE
) );
1201 $page->loadPageData( IDBAccessObject
::READ_NORMAL
);
1202 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_NORMAL
) );
1203 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_LATEST
) );
1204 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_LOCKING
) );
1205 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_EXCLUSIVE
) );
1207 $page->loadPageData( IDBAccessObject
::READ_LATEST
);
1208 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_NORMAL
) );
1209 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_LATEST
) );
1210 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_LOCKING
) );
1211 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_EXCLUSIVE
) );
1213 $page->loadPageData( IDBAccessObject
::READ_LOCKING
);
1214 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_NORMAL
) );
1215 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_LATEST
) );
1216 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_LOCKING
) );
1217 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject
::READ_EXCLUSIVE
) );
1219 $page->loadPageData( IDBAccessObject
::READ_EXCLUSIVE
);
1220 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_NORMAL
) );
1221 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_LATEST
) );
1222 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_LOCKING
) );
1223 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject
::READ_EXCLUSIVE
) );
1226 public function testUpdateCategoryCounts() {
1227 $page = new WikiPage( Title
::newFromText( __METHOD__
) );
1229 // Add an initial category
1230 $page->updateCategoryCounts( [ 'A' ], [], 0 );
1232 $this->assertSame( 1, Category
::newFromName( 'A' )->getMemberCount() );
1233 $this->assertSame( 0, Category
::newFromName( 'B' )->getMemberCount() );
1234 $this->assertSame( 0, Category
::newFromName( 'C' )->getMemberCount() );
1236 // Add a new category
1237 $page->updateCategoryCounts( [ 'B' ], [], 0 );
1239 $this->assertSame( 1, Category
::newFromName( 'A' )->getMemberCount() );
1240 $this->assertSame( 1, Category
::newFromName( 'B' )->getMemberCount() );
1241 $this->assertSame( 0, Category
::newFromName( 'C' )->getMemberCount() );
1243 // Add and remove a category
1244 $page->updateCategoryCounts( [ 'C' ], [ 'A' ], 0 );
1246 $this->assertSame( 0, Category
::newFromName( 'A' )->getMemberCount() );
1247 $this->assertSame( 1, Category
::newFromName( 'B' )->getMemberCount() );
1248 $this->assertSame( 1, Category
::newFromName( 'C' )->getMemberCount() );
1251 public static function provideUpdateRedirectOn() {
1252 yield
[ '#REDIRECT [[Foo]]', true, null, true, true, [] ];
1253 yield
[ '#REDIRECT [[Foo]]', true, 'Foo', true, true, [ [ NS_MAIN
, 'Foo' ] ] ];
1254 yield
[ 'SomeText', false, null, false, true, [] ];
1255 yield
[ 'SomeText', false, 'Foo', false, true, [ [ NS_MAIN
, 'Foo' ] ] ];
1259 * @dataProvider provideUpdateRedirectOn
1261 * @param string $initialText
1262 * @param bool $initialRedirectState
1263 * @param string|null $redirectTitle
1264 * @param bool|null $lastRevIsRedirect
1265 * @param bool $expectedSuccess
1266 * @param array $expectedRows
1268 public function testUpdateRedirectOn(
1270 $initialRedirectState,
1276 static $pageCounter = 0;
1279 $page = $this->createPage( Title
::newFromText( __METHOD__
. $pageCounter ), $initialText );
1280 $this->assertSame( $initialRedirectState, $page->isRedirect() );
1282 $redirectTitle = is_string( $redirectTitle )
1283 ? Title
::newFromText( $redirectTitle )
1286 $success = $this->getServiceContainer()->getRedirectStore()
1287 ->updateRedirectTarget( $page, $redirectTitle, $lastRevIsRedirect );
1288 $this->assertSame( $expectedSuccess, $success, 'Success assertion' );
1290 * updateRedirectTarget explicitly updates the redirect table (and not the page table).
1291 * Most of core checks the page table for redirect status, so we have to be ugly and
1292 * assert a select from the table here.
1294 $this->assertRedirectTableCountForPageId( $page->getId(), $expectedRows );
1297 private function assertRedirectTableCountForPageId( $pageId, $expectedRows ) {
1298 $this->newSelectQueryBuilder()
1299 ->select( [ 'rd_namespace', 'rd_title' ] )
1300 ->from( 'redirect' )
1301 ->where( [ 'rd_from' => $pageId ] )
1302 ->assertResultSet( $expectedRows );
1305 public function testInsertRedirectEntry_insertsRedirectEntry() {
1306 $page = $this->createPage( Title
::newFromText( __METHOD__
), 'A' );
1307 $this->assertRedirectTableCountForPageId( $page->getId(), [] );
1309 $targetTitle = Title
::newFromText( 'SomeTarget#Frag' );
1310 $reflectedTitle = TestingAccessWrapper
::newFromObject( $targetTitle );
1311 $reflectedTitle->mInterwiki
= 'eninter';
1312 $this->getServiceContainer()->getRedirectStore()
1313 ->updateRedirectTarget( $page, $targetTitle );
1315 $this->newSelectQueryBuilder()
1316 ->select( [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ] )
1317 ->from( 'redirect' )
1318 ->where( [ 'rd_from' => $page->getId() ] )
1319 ->assertResultSet( [ [
1320 strval( $page->getId() ),
1321 strval( $targetTitle->getNamespace() ),
1322 strval( $targetTitle->getDBkey() ),
1323 strval( $targetTitle->getFragment() ),
1324 strval( $targetTitle->getInterwiki() ),
1328 public function testInsertRedirectEntry_T278367() {
1329 $page = $this->createPage( Title
::newFromText( __METHOD__
), 'A' );
1330 $this->assertRedirectTableCountForPageId( $page->getId(), [] );
1332 $targetTitle = Title
::newFromText( '#Frag' );
1333 $ok = $this->getServiceContainer()->getRedirectStore()
1334 ->updateRedirectTarget( $page, $targetTitle );
1336 $this->assertFalse( $ok );
1337 $this->assertRedirectTableCountForPageId( $page->getId(), [] );
1340 public function testUpdateRevisionOn_existingPage() {
1341 $user = $this->getTestSysop()->getUser();
1342 $page = $this->createPage( __METHOD__
, 'StartText' );
1344 $revisionRecord = new MutableRevisionRecord( $page );
1345 $revisionRecord->setContent(
1347 new WikitextContent( __METHOD__
. '-text' )
1349 $revisionRecord->setUser( $user );
1350 $revisionRecord->setTimestamp( '20170707040404' );
1351 $revisionRecord->setPageId( $page->getId() );
1352 $revisionRecord->setId( 9989 );
1353 $revisionRecord->setSize( strlen( __METHOD__
. '-text' ) );
1354 $revisionRecord->setMinorEdit( true );
1355 $revisionRecord->setComment( CommentStoreComment
::newUnsavedComment( __METHOD__
) );
1357 $result = $page->updateRevisionOn( $this->getDb(), $revisionRecord );
1358 $this->assertTrue( $result );
1359 $this->assertSame( 9989, $page->getLatest() );
1360 $this->assertEquals( $revisionRecord, $page->getRevisionRecord() );
1363 public function testUpdateRevisionOn_NonExistingPage() {
1364 $user = $this->getTestSysop()->getUser();
1365 $page = $this->createPage( __METHOD__
, 'StartText' );
1366 $this->deletePage( $page, '', $user );
1368 $revisionRecord = new MutableRevisionRecord( $page );
1369 $revisionRecord->setContent(
1371 new WikitextContent( __METHOD__
. '-text' )
1373 $revisionRecord->setUser( $user );
1374 $revisionRecord->setTimestamp( '20170707040404' );
1375 $revisionRecord->setPageId( $page->getId() );
1376 $revisionRecord->setId( 9989 );
1377 $revisionRecord->setSize( strlen( __METHOD__
. '-text' ) );
1378 $revisionRecord->setMinorEdit( true );
1379 $revisionRecord->setComment( CommentStoreComment
::newUnsavedComment( __METHOD__
) );
1381 $result = $page->updateRevisionOn( $this->getDb(), $revisionRecord );
1382 $this->assertFalse( $result );
1385 public function testInsertOn() {
1386 $title = Title
::newFromText( __METHOD__
);
1387 $page = new WikiPage( $title );
1389 $startTimeStamp = wfTimestampNow();
1390 $result = $page->insertOn( $this->getDb() );
1391 $endTimeStamp = wfTimestampNow();
1393 $this->assertIsInt( $result );
1394 $this->assertGreaterThan( 0, $result );
1396 $condition = [ 'page_id' => $result ];
1398 // Check the default fields have been filled
1399 $this->newSelectQueryBuilder()
1409 ->where( $condition )
1410 ->assertResultSet( [ [
1419 // Check the page_random field has been filled
1420 $pageRandom = $this->getDb()->newSelectQueryBuilder()
1421 ->select( 'page_random' )
1423 ->where( $condition )
1425 $this->assertTrue( (float)$pageRandom < 1 && (float)$pageRandom > 0 );
1427 // Assert the touched timestamp in the DB is roughly when we inserted the page
1428 $pageTouched = $this->getDb()->newSelectQueryBuilder()
1429 ->select( 'page_touched' )
1431 ->where( $condition )
1434 wfTimestamp( TS_UNIX
, $startTimeStamp )
1435 <= wfTimestamp( TS_UNIX
, $pageTouched )
1438 wfTimestamp( TS_UNIX
, $endTimeStamp )
1439 >= wfTimestamp( TS_UNIX
, $pageTouched )
1442 // Try inserting the same page again and checking the result is false (no change)
1443 $result = $page->insertOn( $this->getDb() );
1444 $this->assertFalse( $result );
1447 public function testInsertOn_idSpecified() {
1448 $title = Title
::newFromText( __METHOD__
);
1449 $page = new WikiPage( $title );
1452 $result = $page->insertOn( $this->getDb(), $id );
1454 $this->assertSame( $id, $result );
1456 $condition = [ 'page_id' => $result ];
1458 // Check there is actually a row in the db
1459 $this->newSelectQueryBuilder()
1460 ->select( 'page_title' )
1462 ->where( $condition )
1463 ->assertResultSet( [ [ __METHOD__
] ] );
1466 public static function provideTestDoUpdateRestrictions_setBasicRestrictions() {
1467 // Note: Once the current dates passes the date in these tests they will fail.
1468 yield
'move something' => [
1470 [ 'move' => 'something' ],
1472 [ 'edit' => [], 'move' => [ 'something' ] ],
1475 yield
'move something, edit blank' => [
1477 [ 'move' => 'something', 'edit' => '' ],
1479 [ 'edit' => [], 'move' => [ 'something' ] ],
1482 yield
'edit sysop, with expiry' => [
1484 [ 'edit' => 'sysop' ],
1485 [ 'edit' => '21330101020202' ],
1486 [ 'edit' => [ 'sysop' ], 'move' => [] ],
1487 [ 'edit' => '21330101020202' ],
1489 yield
'move and edit, move with expiry' => [
1491 [ 'move' => 'something', 'edit' => 'another' ],
1492 [ 'move' => '22220202010101' ],
1493 [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
1494 [ 'move' => '22220202010101' ],
1496 yield
'move and edit, edit with infinity expiry' => [
1498 [ 'move' => 'something', 'edit' => 'another' ],
1499 [ 'edit' => 'infinity' ],
1500 [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
1501 [ 'edit' => 'infinity' ],
1503 yield
'non existing, create something' => [
1505 [ 'create' => 'something' ],
1507 [ 'create' => [ 'something' ] ],
1510 yield
'non existing, create something with expiry' => [
1512 [ 'create' => 'something' ],
1513 [ 'create' => '23451212112233' ],
1514 [ 'create' => [ 'something' ] ],
1515 [ 'create' => '23451212112233' ],
1520 * @dataProvider provideTestDoUpdateRestrictions_setBasicRestrictions
1522 public function testDoUpdateRestrictions_setBasicRestrictions(
1526 array $expectedRestrictions,
1527 array $expectedRestrictionExpiries
1529 if ( $pageExists ) {
1530 $page = $this->createPage( __METHOD__
, 'ABC' );
1532 $page = $this->getNonexistingTestPage( Title
::newFromText( __METHOD__
. '-nonexist' ) );
1534 $user = $this->getTestSysop()->getUser();
1535 $userIdentity = $this->getTestSysop()->getUserIdentity();
1539 // Expect that the onArticleProtect and onArticleProtectComplete hooks are called for successful calls.
1540 $articleProtectHookCalled = false;
1541 $this->setTemporaryHook(
1543 static function () use ( &$articleProtectHookCalled ) {
1544 $articleProtectHookCalled = true;
1549 $articleProtectCompleteHookCalled = false;
1550 $this->setTemporaryHook(
1551 'ArticleProtectComplete',
1552 static function () use ( &$articleProtectCompleteHookCalled ) {
1553 $articleProtectCompleteHookCalled = true;
1558 $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, 'aReason', $userIdentity, [] );
1560 $this->assertTrue( $articleProtectCompleteHookCalled );
1561 $this->assertTrue( $articleProtectHookCalled );
1563 $logId = $status->getValue();
1564 $restrictionStore = $this->getServiceContainer()->getRestrictionStore();
1565 $allRestrictions = $restrictionStore->getAllRestrictions( $page->getTitle() );
1567 $this->assertStatusGood( $status );
1568 $this->assertIsInt( $logId );
1569 $this->assertSame( $expectedRestrictions, $allRestrictions );
1570 foreach ( $expectedRestrictionExpiries as $key => $value ) {
1571 $this->assertSame( $value, $restrictionStore->getRestrictionExpiry( $page->getTitle(), $key ) );
1574 // Make sure the log entry looks good
1575 // log_params is not checked here
1576 $commentQuery = $this->getServiceContainer()->getCommentStore()->getJoin( 'log_comment' );
1577 $this->newSelectQueryBuilder()
1579 'log_comment' => $commentQuery['fields']['log_comment_text'],
1585 ->tables( $commentQuery['tables'] )
1586 ->where( [ 'log_id' => $logId ] )
1587 ->joinConds( $commentQuery['joins'] )
1590 (string)$user->getActorId(),
1591 (string)$page->getTitle()->getNamespace(),
1592 $page->getTitle()->getDBkey(),
1596 public function testDoUpdateRestrictions_failsOnReadOnly() {
1597 $page = $this->createPage( __METHOD__
, 'ABC' );
1598 $user = $this->getTestSysop()->getUser();
1602 $readOnly = $this->getDummyReadOnlyMode( true );
1603 $this->setService( 'ReadOnlyMode', $readOnly );
1605 $status = $page->doUpdateRestrictions( [], [], $cascade, 'aReason', $user, [] );
1606 $this->assertStatusError( 'readonlytext', $status );
1609 public function testDoUpdateRestrictions_returnsGoodIfNothingChanged() {
1610 $page = $this->createPage( __METHOD__
, 'ABC' );
1611 $user = $this->getTestSysop()->getUser();
1613 $limit = [ 'edit' => 'sysop' ];
1615 $status = $page->doUpdateRestrictions(
1624 // The first entry should have a logId as it did something
1625 $this->assertStatusGood( $status );
1626 $this->assertIsInt( $status->getValue() );
1628 $status = $page->doUpdateRestrictions(
1637 // The second entry should not have a logId as nothing changed
1638 $this->assertStatusGood( $status );
1639 $this->assertNull( $status->getValue() );
1642 public function testDoUpdateRestrictions_logEntryTypeAndAction() {
1643 $page = $this->createPage( __METHOD__
, 'ABC' );
1644 $user = $this->getTestSysop()->getUser();
1648 $status = $page->doUpdateRestrictions(
1649 [ 'edit' => 'sysop' ],
1656 $this->assertStatusGood( $status );
1657 $this->assertIsInt( $status->getValue() );
1658 $this->newSelectQueryBuilder()
1659 ->select( [ 'log_type', 'log_action' ] )
1661 ->where( [ 'log_id' => $status->getValue() ] )
1662 ->assertResultSet( [ [ 'protect', 'protect' ] ] );
1664 // Modify the protection
1665 $status = $page->doUpdateRestrictions(
1666 [ 'edit' => 'somethingElse' ],
1673 $this->assertStatusGood( $status );
1674 $this->assertIsInt( $status->getValue() );
1675 $this->newSelectQueryBuilder()
1676 ->select( [ 'log_type', 'log_action' ] )
1678 ->where( [ 'log_id' => $status->getValue() ] )
1679 ->assertResultSet( [ [ 'protect', 'modify' ] ] );
1681 // Remove the protection
1682 $status = $page->doUpdateRestrictions(
1690 $this->assertStatusGood( $status );
1691 $this->assertIsInt( $status->getValue() );
1692 $this->newSelectQueryBuilder()
1693 ->select( [ 'log_type', 'log_action' ] )
1695 ->where( [ 'log_id' => $status->getValue() ] )
1696 ->assertResultSet( [ [ 'protect', 'unprotect' ] ] );
1699 public function testNewPageUpdater() {
1700 $user = $this->getTestUser()->getUser();
1701 $page = $this->newPage( __METHOD__
, __METHOD__
);
1702 $content = new WikitextContent( 'Hello World' );
1704 /** @var ContentRenderer $contentRenderer */
1705 $contentRenderer = $this->getMockBuilder( ContentRenderer
::class )
1706 ->onlyMethods( [ 'getParserOutput' ] )
1707 ->disableOriginalConstructor()
1709 $contentRenderer->expects( $this->once() )
1710 ->method( 'getParserOutput' )
1711 ->willReturn( new ParserOutput( 'HTML' ) );
1713 $this->setService( 'ContentRenderer', $contentRenderer );
1715 $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user );
1716 $preparedUpdateBefore = $page->getCurrentUpdate();
1718 // provide context, so the cache can be kept in place
1719 $slotsUpdate = new revisionSlotsUpdate();
1720 $slotsUpdate->modifyContent( SlotRecord
::MAIN
, $content );
1722 $revision = $page->newPageUpdater( $user, $slotsUpdate )
1723 ->setContent( SlotRecord
::MAIN
, $content )
1724 ->saveRevision( CommentStoreComment
::newUnsavedComment( 'test' ), EDIT_NEW
);
1726 $preparedEditAfter = $page->prepareContentForEdit( $content, $revision, $user );
1727 $preparedUpdateAfter = $page->getCurrentUpdate();
1729 $this->assertSame( $revision->getId(), $page->getLatest() );
1731 // Parsed output must remain cached throughout.
1733 $preparedEditBefore->output
,
1734 $preparedEditAfter->output
1737 $preparedEditBefore->output
,
1738 $preparedUpdateBefore->getCanonicalParserOutput()
1741 $preparedEditBefore->output
,
1742 $preparedUpdateAfter->getCanonicalParserOutput()
1746 public function testGetDerivedDataUpdater() {
1747 $admin = $this->getTestSysop()->getUser();
1749 /** @var object $page */
1750 $page = $this->createPage( __METHOD__
, __METHOD__
);
1751 $page = TestingAccessWrapper
::newFromObject( $page );
1753 $revision = $page->getRevisionRecord();
1754 $user = $revision->getUser();
1756 $slotsUpdate = new RevisionSlotsUpdate();
1757 $slotsUpdate->modifyContent( SlotRecord
::MAIN
, new WikitextContent( 'Hello World' ) );
1759 // get a virgin updater
1760 $updater1 = $page->getDerivedDataUpdater( $user );
1761 $this->assertFalse( $updater1->isUpdatePrepared() );
1763 $updater1->prepareUpdate( $revision );
1765 // Re-use updater with same revision or content, even if base changed
1766 $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, $revision ) );
1768 $slotsUpdate = RevisionSlotsUpdate
::newFromContent(
1769 [ SlotRecord
::MAIN
=> $revision->getContent( SlotRecord
::MAIN
) ]
1771 $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, null, $slotsUpdate ) );
1773 // Don't re-use for edit if base revision ID changed
1774 $this->assertNotSame(
1776 $page->getDerivedDataUpdater( $user, null, $slotsUpdate, true )
1779 // Don't re-use with different user
1780 $updater2a = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
1781 $updater2a->prepareContent( $admin, $slotsUpdate, false );
1783 $updater2b = $page->getDerivedDataUpdater( $user, null, $slotsUpdate );
1784 $updater2b->prepareContent( $user, $slotsUpdate, false );
1785 $this->assertNotSame( $updater2a, $updater2b );
1787 // Don't re-use with different content
1788 $updater3 = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
1789 $updater3->prepareUpdate( $revision );
1790 $this->assertNotSame( $updater2b, $updater3 );
1792 // Don't re-use if no context given
1793 $updater4 = $page->getDerivedDataUpdater( $admin );
1794 $updater4->prepareUpdate( $revision );
1795 $this->assertNotSame( $updater3, $updater4 );
1797 // Don't re-use if AGAIN no context given
1798 $updater5 = $page->getDerivedDataUpdater( $admin );
1799 $this->assertNotSame( $updater4, $updater5 );
1801 // Don't re-use cached "virgin" unprepared updater
1802 $updater6 = $page->getDerivedDataUpdater( $admin, $revision );
1803 $this->assertNotSame( $updater5, $updater6 );
1806 protected function assertPreparedEditEquals(
1807 PreparedEdit
$edit, PreparedEdit
$edit2, $message = ''
1809 // suppress differences caused by a clock tick between generating the two PreparedEdits
1810 $timestamp1 = $edit->getOutput()->getCacheTime();
1811 $timestamp2 = $edit2->getOutput()->getCacheTime();
1812 $this->assertEquals( $edit, $edit2, $message );
1813 $this->assertLessThan( 3, abs( $timestamp1 - $timestamp2 ), $message );
1816 protected function assertPreparedEditNotEquals(
1817 PreparedEdit
$edit, PreparedEdit
$edit2, $message = ''
1819 $this->assertNotEquals( $edit, $edit2, $message );
1823 * This is just to confirm that WikiPage::updateRevisionOn() updates the
1824 * Title and LinkCache with the correct redirect value. Failing to do so
1825 * causes subtle test failures in extensions, such as Cognate (T283654)
1826 * and Echo (no task, but see code review of I12542fc899).
1828 public function testUpdateSetsTitleRedirectCache() {
1829 // Get a title object without using the title cache
1830 $title = Title
::makeTitleSafe( NS_MAIN
, 'A new redirect' );
1831 $this->assertFalse( $title->isRedirect() );
1833 $dbw = $this->getDb();
1834 $store = $this->getServiceContainer()->getRevisionStore();
1835 $page = $this->newPage( $title );
1836 $page->insertOn( $dbw );
1838 $revision = new MutableRevisionRecord( $page );
1839 $revision->setContent(
1841 new WikitextContent( '#REDIRECT [[Target]]' )
1843 $revision->setTimestamp( wfTimestampNow() );
1844 $revision->setComment( CommentStoreComment
::newUnsavedComment( '' ) );
1845 $revision->setUser( $this->getTestUser()->getUser() );
1847 $revision = $store->insertRevisionOn( $revision, $dbw );
1849 $page->updateRevisionOn( $dbw, $revision );
1850 // check the title cache
1851 $this->assertTrue( $title->isRedirect() );
1852 // check the link cache with a fresh title
1853 $title = Title
::makeTitleSafe( NS_MAIN
, 'A new redirect' );
1854 $this->assertTrue( $title->isRedirect() );
1857 public function testGetTitle() {
1858 $page = $this->createPage( __METHOD__
, 'whatever' );
1860 $title = $page->getTitle();
1861 $this->assertSame( __METHOD__
, $title->getText() );
1863 $this->assertSame( $page->getId(), $title->getId() );
1864 $this->assertSame( $page->getNamespace(), $title->getNamespace() );
1865 $this->assertSame( $page->getDBkey(), $title->getDBkey() );
1866 $this->assertSame( $page->getWikiId(), $title->getWikiId() );
1867 $this->assertSame( $page->canExist(), $title->canExist() );
1870 public function testToPageRecord() {
1871 $page = $this->createPage( __METHOD__
, 'whatever' );
1872 $record = $page->toPageRecord();
1874 $this->assertSame( $page->getId(), $record->getId() );
1875 $this->assertSame( $page->getNamespace(), $record->getNamespace() );
1876 $this->assertSame( $page->getDBkey(), $record->getDBkey() );
1877 $this->assertSame( $page->getWikiId(), $record->getWikiId() );
1878 $this->assertSame( $page->canExist(), $record->canExist() );
1880 $this->assertSame( $page->getLatest(), $record->getLatest() );
1881 $this->assertSame( $page->getTouched(), $record->getTouched() );
1882 $this->assertSame( $page->isNew(), $record->isNew() );
1883 $this->assertSame( $page->isRedirect(), $record->isRedirect() );
1886 public function testGetTouched() {
1887 $page = $this->createPage( __METHOD__
, 'whatever' );
1889 $touched = $this->getDb()->newSelectQueryBuilder()
1890 ->select( 'page_touched' )
1892 ->where( [ 'page_id' => $page->getId() ] )
1894 $touched = MWTimestamp
::convert( TS_MW
, $touched );
1896 // Internal cache of the touched time was set after the page was created
1897 $this->assertSame( $touched, $page->getTouched() );
1899 $touched = MWTimestamp
::convert( TS_MW
, MWTimestamp
::convert( TS_UNIX
, $touched ) +
100 );
1900 $page->getTitle()->invalidateCache( $touched );
1902 // Re-load touched time
1903 $page = $this->newPage( $page->getTitle() );
1904 $this->assertSame( $touched, $page->getTouched() );
1906 // Cause the latest revision to be loaded
1907 $page->getRevisionRecord();
1909 // Make sure the internal cache of the touched time was not overwritten
1910 $this->assertSame( $touched, $page->getTouched() );