18 parent::__construct( $name,
$data, $dataName );
64 'wgMultiContentRevisionSchemaMigrationStage',
67 $this->pagesToDelete = [];
73 foreach ( $this->pagesToDelete as $p ) {
78 $p->doDeleteArticle(
"testing done." );
94 private function newPage( $title, $model =
null ) {
97 $title = Title::newFromText( $title, $ns );
102 $this->pagesToDelete[] =
$p;
116 $page = $this->
newPage( $page, $model );
124 $content = ContentHandler::makeContent(
$content, $page->getTitle(), $model );
131 $updater = $page->newPageUpdater( $user );
133 foreach (
$content as $role => $cnt ) {
134 $updater->setContent( $role, $cnt );
137 $updater->saveRevision( CommentStoreComment::newUnsavedComment(
"testing" ) );
139 $this->fail(
$updater->getStatus()->getWikiText() );
150 $sysop = $this->
getTestUser( [
'sysop' ] )->getUser();
152 $page = $this->
createPage( __METHOD__, __METHOD__,
null, $user );
153 $title = $page->getTitle();
155 $content = ContentHandler::makeContent(
156 "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
157 .
" nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
161 $content2 = ContentHandler::makeContent(
162 "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
163 .
"Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
168 $edit = $page->prepareContentForEdit(
$content,
null, $user,
null,
false );
171 ParserOptions::class,
175 $this->
assertContains(
'</a>', $edit->output->getText(),
"output" );
177 'consetetur sadipscing elitr',
178 $edit->output->getText(),
184 $this->
assertSame( $edit->output, $edit->output,
"output field" );
185 $this->
assertSame( $edit->popts, $edit->popts,
"popts field" );
186 $this->
assertSame(
null, $edit->revid,
"revid field" );
189 $sameEdit = $page->prepareContentForEdit(
$content,
null, $user,
null,
false );
191 $this->
assertSame( $edit->pstContent, $sameEdit->pstContent,
're-use output' );
192 $this->
assertSame( $edit->output, $sameEdit->output,
're-use output' );
195 $rev = $page->getRevision();
196 $edit2 = $page->prepareContentForEdit( $content2,
null, $user,
null,
false );
198 $this->
assertContains(
'At vero eos', $edit2->pstContent->serialize(),
"content" );
201 $this->
assertContains(
'[[gubergren]]', $edit2->pstContent->serialize() );
204 $edit3 = $page->prepareContentForEdit( $content2,
null, $sysop,
null,
false );
219 $siteStatsUpdate = SiteStatsUpdate::factory(
220 [
'edits' => 1000,
'articles' => 1000,
'pages' => 1000 ]
222 $siteStatsUpdate->doUpdate();
224 $page = $this->
createPage( __METHOD__, __METHOD__ );
229 'page' => $page->getId(),
230 'title' => $page->getTitle(),
231 'comment' => __METHOD__,
232 'minor_edit' =>
true,
233 'text' => __METHOD__ .
' [[|foo]][[bar]]',
234 'user' => $user->getId(),
235 'user_text' => $user->getName(),
236 'timestamp' =>
'20170707040404',
242 $page->doEditUpdates( $revision, $user );
247 $res =
$dbr->select(
'pagelinks',
'*', [
'pl_from' => $page->getId() ] );
248 $n =
$res->numRows();
251 $this->
assertEquals( 1, $n,
'pagelinks should contain only one link if PST was not applied' );
261 $page = $this->
newPage( __METHOD__ );
262 $title = $page->getTitle();
266 $user2 = $this->
getTestUser( [
'confirmed' ] )->getUser();
268 $content = ContentHandler::makeContent(
269 "[[Lorem ipsum]] dolor sit amet, consetetur sadipscing elitr, sed diam "
270 .
" nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
275 $preparedEditBefore = $page->prepareContentForEdit(
$content,
null, $user1 );
282 $this->
assertSame(
$status->value[
'revision']->getId(), $page->getRevision()->getId() );
283 $this->
assertSame(
$status->value[
'revision']->getSha1(), $page->getRevision()->getSha1() );
286 $rev = $page->getRevision();
287 $preparedEditAfter = $page->prepareContentForEdit(
$content,
$rev, $user1 );
290 $this->
assertSame(
$rev->getId(), (
int)
$rev->getRecentChange()->getAttribute(
'rc_this_oldid' ) );
293 $this->
assertSame( $preparedEditBefore->output, $preparedEditAfter->output );
295 $id = $page->getId();
300 [
'log_type',
'log_action' ],
301 [
'log_page' => $id ],
302 [ [
'create',
'create' ] ]
305 $this->
assertTrue( $title->getArticleID() > 0,
"Title object should have new page id" );
306 $this->
assertTrue( $id > 0,
"WikiPage should have new page id" );
307 $this->
assertTrue( $title->exists(),
"Title object should indicate that the page now exists" );
308 $this->
assertTrue( $page->exists(),
"WikiPage object should indicate that the page now exists" );
310 # ------------------------
312 $res =
$dbr->select(
'pagelinks',
'*', [
'pl_from' => $id ] );
313 $n =
$res->numRows();
316 $this->
assertEquals( 1, $n,
'pagelinks should contain one link from the page' );
318 # ------------------------
321 $retrieved = $page->getContent();
322 $this->
assertTrue(
$content->equals( $retrieved ),
'retrieved content doesn\'t equal original' );
324 # ------------------------
333 $this->
assertTrue( $page->getRevision()->getContent()->equals(
$content ),
'equals' );
335 # ------------------------
336 $content = ContentHandler::makeContent(
337 "At vero eos et accusam et justo duo [[dolores]] et ea rebum. "
338 .
"Stet clita kasd [[gubergren]], no sea takimata sanctus est. ~~~~",
347 $this->
assertSame(
$status->value[
'revision']->getId(), $page->getRevision()->getId() );
348 $this->
assertSame(
$status->value[
'revision']->getSha1(), $page->getRevision()->getSha1() );
351 'not equals (PST must substitute signature)'
354 $rev = $page->getRevision();
356 $this->
assertSame(
$rev->getId(), (
int)
$rev->getRecentChange()->getAttribute(
'rc_this_oldid' ) );
358 # ------------------------
361 $retrieved = $page->getContent();
362 $newText = $retrieved->serialize();
363 $this->
assertContains(
'[[gubergren]]', $newText,
'New text must replace old text.' );
366 # ------------------------
368 $res =
$dbr->select(
'pagelinks',
'*', [
'pl_from' => $id ] );
369 $n =
$res->numRows();
372 $this->
assertEquals( 2, $n,
'pagelinks should contain two links from the page' );
379 $title = Title::newFromText( __METHOD__ );
380 $page = WikiPage::factory( $title );
381 $content = ContentHandler::makeContent(
'$1 van $2', $title );
385 $status1 = $page->doEditContent(
$content, __METHOD__ );
386 $status2 = $page->doEditContent(
$content, __METHOD__ );
405 "[[original text]] foo",
408 $id = $page->getId();
410 $page->doDeleteArticle(
"testing deletion" );
413 $page->getTitle()->getArticleID() > 0,
414 "Title object should now have page id 0"
416 $this->
assertFalse( $page->getId() > 0,
"WikiPage should now have page id 0" );
419 "WikiPage::exists should return false after page was deleted"
423 "WikiPage::getContent should return null after page was deleted"
426 $t = Title::newFromText( $page->getTitle()->getPrefixedText() );
429 "Title::exists should return false after page was deleted"
433 JobQueueGroup::destroySingletons();
438 # ------------------------
440 $res =
$dbr->select(
'pagelinks',
'*', [
'pl_from' => $id ] );
441 $n =
$res->numRows();
444 $this->
assertEquals( 0, $n,
'pagelinks should contain no more links from the page' );
453 "[[original text]] foo",
456 $id = $page->getId();
459 $status = $page->doDeleteArticleReal(
460 "testing user 0 deletion",
468 $actorQuery = ActorMigration::newMigration()->getJoin(
'log_user' );
469 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin(
'log_comment' );
471 [
'logging' ] + $actorQuery[
'tables'] + $commentQuery[
'tables'],
475 'log_comment' => $commentQuery[
'fields'][
'log_comment_text'],
476 'log_user' => $actorQuery[
'fields'][
'log_user'],
477 'log_user_text' => $actorQuery[
'fields'][
'log_user_text'],
481 [
'log_id' => $logId ],
485 'testing user 0 deletion',
488 (
string)$page->getTitle()->getNamespace(),
489 $page->getTitle()->getDBkey(),
492 $actorQuery[
'joins'] + $commentQuery[
'joins']
502 "[[original text]] foo",
505 $id = $page->getId();
509 $status = $page->doDeleteArticleReal(
510 "testing sysop deletion",
518 $actorQuery = ActorMigration::newMigration()->getJoin(
'log_user' );
519 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin(
'log_comment' );
521 [
'logging' ] + $actorQuery[
'tables'] + $commentQuery[
'tables'],
525 'log_comment' => $commentQuery[
'fields'][
'log_comment_text'],
526 'log_user' => $actorQuery[
'fields'][
'log_user'],
527 'log_user_text' => $actorQuery[
'fields'][
'log_user_text'],
531 [
'log_id' => $logId ],
535 'testing sysop deletion',
536 (
string)$user->getId(),
538 (
string)$page->getTitle()->getNamespace(),
539 $page->getTitle()->getDBkey(),
542 $actorQuery[
'joins'] + $commentQuery[
'joins']
554 "[[original text]] foo",
557 $id = $page->getId();
561 $status = $page->doDeleteArticleReal(
570 $actorQuery = ActorMigration::newMigration()->getJoin(
'log_user' );
571 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin(
'log_comment' );
573 [
'logging' ] + $actorQuery[
'tables'] + $commentQuery[
'tables'],
577 'log_comment' => $commentQuery[
'fields'][
'log_comment_text'],
578 'log_user' => $actorQuery[
'fields'][
'log_user'],
579 'log_user_text' => $actorQuery[
'fields'][
'log_user_text'],
583 [
'log_id' => $logId ],
588 (
string)$user->getId(),
590 (
string)$page->getTitle()->getNamespace(),
591 $page->getTitle()->getDBkey(),
594 $actorQuery[
'joins'] + $commentQuery[
'joins']
599 "WikiPage::getContent should return null after the page was suppressed for general users"
604 "WikiPage::getContent should return null after the page was suppressed for user zero"
609 "WikiPage::getContent should return null after the page was suppressed even for a sysop"
620 "[[original text]] foo",
623 $id = $page->getId();
624 $page->loadPageData();
628 $page->doDeleteUpdates( $page->getId(), $page->getContent(), $page->getRevision(), $user );
631 JobQueueGroup::destroySingletons();
636 # ------------------------
638 $res =
$dbr->select(
'pagelinks',
'*', [
'pl_from' => $id ] );
639 $n =
$res->numRows();
642 $this->
assertEquals( 0, $n,
'pagelinks should contain no more links from the page' );
653 ->setConstructorArgs( [ $name ] )
655 [
'getSecondaryDataUpdates',
'getDeletionUpdates',
'unserializeContent' ]
660 $dataUpdate->_name =
"$name data update";
663 $deletionUpdate->_name =
"$name deletion update";
665 $handler->method(
'getSecondaryDataUpdates' )->willReturn( [ $dataUpdate ] );
666 $handler->method(
'getDeletionUpdates' )->willReturn( [ $deletionUpdate ] );
667 $handler->method(
'unserializeContent' )->willReturnCallback(
668 function ( $text ) use (
$handler ) {
674 'wgContentHandlers', [
675 $name =>
function () use (
$handler ){
693 ->setConstructorArgs( [ $text ] )
694 ->setMethods( [
'getModel',
'getContentHandler' ] )
708 $page =
new WikiPage( Title::newFromText( __METHOD__ ) );
711 [
'main' => $mainContent1 ]
714 $dataUpdates = $page->getDeletionUpdates( $page->getRevisionRecord() );
717 $updateNames =
array_map(
function ( $du ) {
721 $this->
assertContains( LinksDeletionUpdate::class, $updateNames );
729 $page = $this->
newPage( __METHOD__ );
731 $rev = $page->getRevision();
737 $rev = $page->getRevision();
747 $page = $this->
newPage( __METHOD__ );
763 $page = $this->
newPage( __METHOD__ );
770 $page =
new WikiPage( $page->getTitle() );
774 $page->doDeleteArticle(
"done testing" );
777 $page =
new WikiPage( $page->getTitle() );
783 [
'WikiPageTest_testHasViewableContent',
false,
true ],
784 [
'Special:WikiPageTest_testHasViewableContent',
false ],
785 [
'MediaWiki:WikiPageTest_testHasViewableContent',
false ],
786 [
'Special:Userlogin',
true ],
787 [
'MediaWiki:help',
true ],
796 $page = $this->
newPage( $title );
797 $this->
assertEquals( $viewable, $page->hasViewableContent() );
801 $this->
assertTrue( $page->hasViewableContent() );
803 $page =
new WikiPage( $page->getTitle() );
804 $this->
assertTrue( $page->hasViewableContent() );
812 'WikiPageTest_testGetRedirectTarget_2',
814 "#REDIRECT [[hello world]]",
820 'WikiPageTest_testGetRedirectTarget_3',
822 "#REDIRECT [[Media:hello_world]]",
827 'WikiPageTest_testGetRedirectTarget_4',
830 '#REDIRECT [[Foobar#🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴]]',
832 'Foobar#🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴🏴...'
843 'wgCapitalLinks' =>
true,
846 $page = $this->
createPage( $title, $text, $model );
848 # sanity check, because this test seems to fail for no reason for some people.
849 $c = $page->getContent();
852 # now, test the actual redirect
853 $t = $page->getRedirectTarget();
862 $page = $this->
createPage( $title, $text, $model );
870 [
'WikiPageTest_testIsCountable',
876 [
'WikiPageTest_testIsCountable',
884 [
'WikiPageTest_testIsCountable',
890 [
'WikiPageTest_testIsCountable',
898 [
'WikiPageTest_testIsCountable',
904 [
'WikiPageTest_testIsCountable',
912 [
'Talk:WikiPageTest_testIsCountable',
918 [
'Talk:WikiPageTest_testIsCountable',
926 [
'MediaWiki:WikiPageTest_testIsCountable.js',
932 [
'MediaWiki:WikiPageTest_testIsCountable.js',
950 $title = Title::newFromText( $title );
954 && ContentHandler::getDefaultModelFor( $title ) != $model
956 $this->
markTestSkipped(
"Can not use non-default content model $model for "
957 . $title->getPrefixedDBkey() .
" with \$wgContentHandlerUseDB disabled." );
960 $page = $this->
createPage( $title, $text, $model );
962 $editInfo = $page->prepareContentForEdit( $page->getContent() );
964 $v = $page->isCountable();
965 $w = $page->isCountable( $editInfo );
970 "isCountable( null ) returned unexpected value " .
var_export( $v,
true )
971 .
" instead of " .
var_export( $expected,
true )
972 .
" in mode `$mode` for text \"$text\""
978 "isCountable( \$editInfo ) returned unexpected value " .
var_export( $v,
true )
979 .
" instead of " .
var_export( $expected,
true )
980 .
" in mode `$mode` for text \"$text\""
989 "<div class=\"mw-parser-output\"><p>hello <i>world</i></p></div>"
1000 $page = $this->
createPage( __METHOD__, $text, $model );
1002 $opt = $page->makeParserOptions(
'canonical' );
1003 $po = $page->getParserOutput(
$opt );
1004 $text = $po->getText();
1006 $text =
trim(
preg_replace(
'/<!--.*?-->/sm',
'', $text ) ); # strip injected comments
1007 $text =
preg_replace(
'!\s*(</p>|</div>)!sm',
'\1', $text ); # don
't let tidy confuse us
1009 $this->assertEquals( $expectedHtml, $text );
1017 public function testGetParserOutput_nonexisting() {
1018 $page = new WikiPage( Title::newFromText( __METHOD__ ) );
1020 $opt = new ParserOptions();
1021 $po = $page->getParserOutput( $opt );
1023 $this->assertFalse( $po, "getParserOutput() shall return false for non-existing pages." );
1029 public function testGetParserOutput_badrev() {
1030 $page = $this->createPage( __METHOD__, 'dummy
', CONTENT_MODEL_WIKITEXT );
1032 $opt = new ParserOptions();
1033 $po = $page->getParserOutput( $opt, $page->getLatest() + 1234 );
1035 // @todo would be neat to also test deleted revision
1037 $this->assertFalse( $po, "getParserOutput() shall return false for non-existing revisions." );
1040 public static $sections =
1054 public function dataReplaceSection() {
1055 // NOTE: assume the Help namespace to contain wikitext
1057 [ 'Help:WikiPageTest_testReplaceSection
',
1058 CONTENT_MODEL_WIKITEXT,
1063 trim( preg_replace( '/^Intro/sm
', 'No more
', self::$sections ) )
1065 [ 'Help:WikiPageTest_testReplaceSection
',
1066 CONTENT_MODEL_WIKITEXT,
1073 [ 'Help:WikiPageTest_testReplaceSection
',
1074 CONTENT_MODEL_WIKITEXT,
1077 "== TEST ==\nmore fun",
1079 trim( preg_replace( '/^== test ==.*== foo ==/sm
',
1080 "== TEST ==\nmore fun\n\n== foo ==",
1083 [ 'Help:WikiPageTest_testReplaceSection
',
1084 CONTENT_MODEL_WIKITEXT,
1089 trim( self::$sections )
1091 [ 'Help:WikiPageTest_testReplaceSection
',
1092 CONTENT_MODEL_WIKITEXT,
1097 trim( self::$sections ) . "\n\n== New ==\n\nNo more"
1106 public function testReplaceSectionContent( $title, $model, $text, $section,
1107 $with, $sectionTitle, $expected
1109 $page = $this->createPage( $title, $text, $model );
1111 $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1113 $c = $page->replaceSectionContent( $section, $content, $sectionTitle );
1115 $this->assertEquals( $expected, $c ? trim( $c->getText() ) : null );
1122 public function testReplaceSectionAtRev( $title, $model, $text, $section,
1123 $with, $sectionTitle, $expected
1125 $page = $this->createPage( $title, $text, $model );
1126 $baseRevId = $page->getLatest();
1128 $content = ContentHandler::makeContent( $with, $page->getTitle(), $page->getContentModel() );
1130 $c = $page->replaceSectionAtRev( $section, $content, $sectionTitle, $baseRevId );
1132 $this->assertEquals( $expected, $c ? trim( $c->getText() ) : null );
1138 public function testGetOldestRevision() {
1139 $page = $this->newPage( __METHOD__ );
1140 $page->doEditContent(
1141 new WikitextContent( 'one
' ),
1145 $rev1 = $page->getRevision();
1147 $page = new WikiPage( $page->getTitle() );
1148 $page->doEditContent(
1149 new WikitextContent( 'two
' ),
1154 $page = new WikiPage( $page->getTitle() );
1155 $page->doEditContent(
1156 new WikitextContent( 'three
' ),
1162 $this->assertNotEquals(
1164 $page->getRevision()->getId(),
1165 '$page->getRevision()->getId()
'
1169 $this->assertEquals(
1171 $page->getOldestRevision()->getId(),
1172 '$page->getOldestRevision()->getId()
'
1180 public function testDoRollback() {
1181 // FIXME: fails under postgres
1182 $this->markTestSkippedIfDbType( 'postgres
' );
1184 $admin = $this->getTestSysop()->getUser();
1185 $user1 = $this->getTestUser()->getUser();
1186 // Use the confirmed group for user2 to make sure the user is different
1187 $user2 = $this->getTestUser( [ 'confirmed
' ] )->getUser();
1189 // make sure we can test autopatrolling
1190 $this->setMwGlobals( 'wgUseRCPatrol
', true );
1192 // TODO: MCR: test rollback of multiple slots!
1193 $page = $this->newPage( __METHOD__ );
1197 $status1 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1198 "section one", EDIT_NEW, false, $admin );
1201 $status2 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1202 "adding section two", 0, false, $user1 );
1204 $text .= "\n\nthree";
1205 $status3 = $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ),
1206 "adding section three", 0, false, $user2 );
1211 $rev1 = $status1->getValue()['revision
'];
1212 $rev2 = $status2->getValue()['revision
'];
1213 $rev3 = $status3->getValue()['revision
'];
1220 $this->assertEquals( 3, Revision::countByPageId( wfGetDB( DB_REPLICA ), $page->getId() ) );
1221 $this->assertEquals( $admin->getName(), $rev1->getUserText() );
1222 $this->assertEquals( $user1->getName(), $rev2->getUserText() );
1223 $this->assertEquals( $user2->getName(), $rev3->getUserText() );
1225 // Now, try the actual rollback
1226 $token = $admin->getEditToken( '
rollback' );
1227 $rollbackErrors = $page->doRollback(
1236 if ( $rollbackErrors ) {
1238 "Rollback failed:\n" .
1239 print_r( $rollbackErrors, true ) . ";\n" .
1240 print_r( $resultDetails, true )
1244 $page = new WikiPage( $page->getTitle() );
1245 $this->assertEquals( $rev2->getSha1(), $page->getRevision()->getSha1(),
1246 "rollback did not revert to the correct revision" );
1247 $this->assertEquals( "one\n\ntwo", $page->getContent()->getText() );
1249 $rc = MediaWikiServices::getInstance()->getRevisionStore()->getRecentChange(
1250 $page->getRevision()->getRevisionRecord()
1253 $this->assertNotNull( $rc, 'RecentChanges entry
' );
1254 $this->assertEquals(
1255 RecentChange::PRC_AUTOPATROLLED,
1256 $rc->getAttribute( 'rc_patrolled
' ),
1260 // TODO: MCR: assert origin once we write slot data
1261 // $mainSlot = $page->getRevision()->getRevisionRecord()->getSlot( SlotRecord::MAIN );
1262 // $this->assertTrue( $mainSlot->isInherited(), 'isInherited
' );
1263 // $this->assertSame( $rev2->getId(), $mainSlot->getOrigin(), 'getOrigin
' );
1270 public function testDoRollbackFailureSameContent() {
1271 $admin = $this->getTestSysop()->getUser();
1274 $page = $this->newPage( __METHOD__ );
1275 $page->doEditContent(
1276 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1282 $rev1 = $page->getRevision();
1284 $user1 = $this->getTestUser( [ '
sysop' ] )->getUser();
1286 $page = new WikiPage( $page->getTitle() );
1287 $page->doEditContent(
1288 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1289 "adding section two",
1295 # now, do a the rollback from the same user was doing the edit before
1296 $resultDetails = [];
1297 $token = $user1->getEditToken( 'rollback' );
1298 $errors = $page->doRollback(
1300 "testing revert same user",
1307 $this->assertEquals( [], $errors, "Rollback failed same user" );
1309 # now, try the rollback
1310 $resultDetails = [];
1311 $token = $admin->getEditToken( 'rollback' );
1312 $errors = $page->doRollback(
1321 $this->assertEquals(
1331 "Rollback not failed"
1334 $page = new WikiPage( $page->getTitle() );
1335 $this->assertEquals( $rev1->getSha1(), $page->getRevision()->getSha1(),
1336 "rollback did not revert to the correct revision" );
1337 $this->assertEquals( "one", $page->getContent()->getText() );
1344 public function testDoRollbackTagging() {
1345 if ( !in_array( 'mw-
rollback', ChangeTags::getSoftwareTags() ) ) {
1346 $this->markTestSkipped( 'Rollback tag
deactivated, skipped the test.
' );
1349 $admin = new User();
1350 $admin->setName( 'Administrator
' );
1351 $admin->addToDatabase();
1353 $text = 'First line
';
1354 $page = $this->newPage( 'WikiPageTest_testDoRollbackTagging
' );
1355 $page->doEditContent(
1356 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1363 $secondUser = new User();
1364 $secondUser->setName( '92.65.217.32
' );
1366 $page = new WikiPage( $page->getTitle() );
1367 $page->doEditContent(
1368 ContentHandler::makeContent( $text, $page->getTitle(), CONTENT_MODEL_WIKITEXT ),
1369 'Adding second line
',
1375 // Now, try the rollback
1376 $admin->addGroup( 'sysop' ); // Make the test user a sysop
1377 $token = $admin->getEditToken( 'rollback' );
1378 $errors = $page->doRollback(
1379 $secondUser->getName(),
1387 // If doRollback completed without errors
1388 if ( $errors === [] ) {
1389 $tags = $resultDetails[ 'tags
' ];
1390 $this->assertContains( 'mw-
rollback', $tags );
1394 public function provideGetAutoDeleteReason() {
1404 [ "first edit", null ],
1406 "/first edit.*only contributor/",
1412 [ "first edit", null ],
1413 [ "second edit", null ],
1415 "/second edit.*only contributor/",
1421 [ "first edit", "127.0.2.22" ],
1422 [ "second edit", "127.0.3.33" ],
1432 . "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam "
1433 . " nonumy eirmod tempor invidunt ut labore et dolore magna "
1434 . "aliquyam erat, sed diam voluptua. At vero eos et accusam "
1435 . "et justo duo dolores et ea rebum. Stet clita kasd gubergren, "
1436 . "no sea takimata sanctus est Lorem ipsum dolor sit amet.'",
1440 '/first edit:.*\.\.\."/
',
1446 [ "first edit", "127.0.2.22" ],
1447 [ "", "127.0.3.33" ],
1449 "/before blanking.*first edit/",
1460 public function testGetAutoDeleteReason( $edits, $expectedResult, $expectedHistory ) {
1463 // NOTE: assume Help namespace to contain wikitext
1464 $page = $this->newPage( "Help:WikiPageTest_testGetAutoDeleteReason" );
1468 foreach ( $edits as $edit ) {
1471 if ( !empty( $edit[1] ) ) {
1472 $user->setName( $edit[1] );
1477 $content = ContentHandler::makeContent( $edit[0], $page->getTitle(), $page->getContentModel() );
1479 $page->doEditContent( $content, "test edit $c", $c < 2 ? EDIT_NEW : 0, false, $user );
1484 $reason = $page->getAutoDeleteReason( $hasHistory );
1486 if ( is_bool( $expectedResult ) || is_null( $expectedResult ) ) {
1487 $this->assertEquals( $expectedResult, $reason );
1489 $this->assertTrue( (bool)preg_match( $expectedResult, $reason ),
1490 "Autosummary didn't match expected pattern
$expectedResult: $reason
" );
1493 $this->assertEquals( $expectedHistory, $hasHistory,
1496 $page->doDeleteArticle( "done
" );
1499 public function providePreSaveTransform() {
1501 [ 'hello this is ~~~',
1502 "hello
this is [[
Special:Contributions/127.0.0.1|127.0.0.1]]
",
1504 [ 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1505 'hello \'\'this\'\' is <nowiki>~~~</nowiki>',
1513 public function testWikiPageFactory() {
1514 $title = Title::makeTitle( NS_FILE, 'Someimage.png' );
1515 $page = WikiPage::factory( $title );
1516 $this->assertEquals( WikiFilePage::class, get_class( $page ) );
1518 $title = Title::makeTitle( NS_CATEGORY, 'SomeCategory' );
1519 $page = WikiPage::factory( $title );
1520 $this->assertEquals( WikiCategoryPage::class, get_class( $page ) );
1522 $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1523 $page = WikiPage::factory( $title );
1524 $this->assertEquals( WikiPage::class, get_class( $page ) );
1531 public function testLoadPageData() {
1532 $title = Title::makeTitle( NS_MAIN, 'SomePage' );
1533 $page = WikiPage::factory( $title );
1535 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1536 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1537 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1538 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1540 $page->loadPageData( IDBAccessObject::READ_NORMAL );
1541 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1542 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1543 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1544 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1546 $page->loadPageData( IDBAccessObject::READ_LATEST );
1547 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1548 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1549 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1550 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1552 $page->loadPageData( IDBAccessObject::READ_LOCKING );
1553 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1554 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1555 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1556 $this->assertFalse( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1558 $page->loadPageData( IDBAccessObject::READ_EXCLUSIVE );
1559 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_NORMAL ) );
1560 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LATEST ) );
1561 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_LOCKING ) );
1562 $this->assertTrue( $page->wasLoadedFrom( IDBAccessObject::READ_EXCLUSIVE ) );
1568 public function testUpdateCategoryCounts() {
1569 $page = new WikiPage( Title::newFromText( __METHOD__ ) );
1571 // Add an initial category
1572 $page->updateCategoryCounts( [ 'A' ], [], 0 );
1574 $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1575 $this->assertEquals( 0, Category::newFromName( 'B' )->getPageCount() );
1576 $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1578 // Add a new category
1579 $page->updateCategoryCounts( [ 'B' ], [], 0 );
1581 $this->assertEquals( 1, Category::newFromName( 'A' )->getPageCount() );
1582 $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1583 $this->assertEquals( 0, Category::newFromName( 'C' )->getPageCount() );
1585 // Add and remove a category
1586 $page->updateCategoryCounts( [ 'C' ], [ 'A' ], 0 );
1588 $this->assertEquals( 0, Category::newFromName( 'A' )->getPageCount() );
1589 $this->assertEquals( 1, Category::newFromName( 'B' )->getPageCount() );
1590 $this->assertEquals( 1, Category::newFromName( 'C' )->getPageCount() );
1593 public function provideUpdateRedirectOn() {
1594 yield [ '#REDIRECT [[Foo]]', true, null, true, true, 0 ];
1595 yield [ '#REDIRECT [[Foo]]', true, 'Foo', true, false, 1 ];
1596 yield [ 'SomeText', false, null, false, true, 0 ];
1597 yield [ 'SomeText', false, 'Foo', false, false, 1 ];
1611 public function testUpdateRedirectOn(
1613 $initialRedirectState,
1619 // FIXME: fails under sqlite and postgres
1620 $this->markTestSkippedIfDbType( 'sqlite' );
1621 $this->markTestSkippedIfDbType( 'postgres' );
1622 static $pageCounter = 0;
1625 $page = $this->createPage( Title::newFromText( __METHOD__ . $pageCounter ), $initialText );
1626 $this->assertSame( $initialRedirectState, $page->isRedirect() );
1628 $redirectTitle = is_string( $redirectTitle )
1629 ? Title::newFromText( $redirectTitle )
1632 $success = $page->updateRedirectOn( $this->db, $redirectTitle, $lastRevIsRedirect );
1633 $this->assertSame( $expectedSuccess, $success, 'Success assertion' );
1639 $this->assertRedirectTableCountForPageId( $page->getId(), $expectedRowCount );
1642 private function assertRedirectTableCountForPageId( $pageId, $expected ) {
1643 $this->assertSelect(
1646 [ 'rd_from' => $pageId ],
1647 [ [ strval( $expected ) ] ]
1654 public function testInsertRedirectEntry_insertsRedirectEntry() {
1655 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1656 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1658 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1659 $targetTitle->mInterwiki = 'eninter';
1660 $page->insertRedirectEntry( $targetTitle, null );
1662 $this->assertSelect(
1664 [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1665 [ 'rd_from' => $page->getId() ],
1667 strval( $page->getId() ),
1668 strval( $targetTitle->getNamespace() ),
1669 strval( $targetTitle->getDBkey() ),
1670 strval( $targetTitle->getFragment() ),
1671 strval( $targetTitle->getInterwiki() ),
1679 public function testInsertRedirectEntry_insertsRedirectEntryWithPageLatest() {
1680 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1681 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1683 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1684 $targetTitle->mInterwiki = 'eninter';
1685 $page->insertRedirectEntry( $targetTitle, $page->getLatest() );
1687 $this->assertSelect(
1689 [ 'rd_from', 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ],
1690 [ 'rd_from' => $page->getId() ],
1692 strval( $page->getId() ),
1693 strval( $targetTitle->getNamespace() ),
1694 strval( $targetTitle->getDBkey() ),
1695 strval( $targetTitle->getFragment() ),
1696 strval( $targetTitle->getInterwiki() ),
1704 public function testInsertRedirectEntry_doesNotInsertIfPageLatestIncorrect() {
1705 $page = $this->createPage( Title::newFromText( __METHOD__ ), 'A' );
1706 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1708 $targetTitle = Title::newFromText( 'SomeTarget#Frag' );
1709 $targetTitle->mInterwiki = 'eninter';
1710 $page->insertRedirectEntry( $targetTitle, 215251 );
1712 $this->assertRedirectTableCountForPageId( $page->getId(), 0 );
1715 private function getRow( array $overrides = [] ) {
1719 'page_is_redirect' => '1',
1720 'page_latest' => '99',
1721 'page_namespace' => '3',
1722 'page_title' => 'JaJaTitle',
1723 'page_restrictions' => 'edit=autoconfirmed,sysop:move=sysop',
1724 'page_touched' => '20120101020202',
1725 'page_links_updated' => '20140101020202',
1727 foreach ( $overrides as $key => $value ) {
1728 $row[$key] = $value;
1730 return (object)$row;
1733 public function provideNewFromRowSuccess() {
1734 yield 'basic row' => [
1736 function ( WikiPage $wikiPage, self $test ) {
1737 $test->assertSame( 44, $wikiPage->getId() );
1738 $test->assertSame( 76, $wikiPage->getTitle()->getLength() );
1739 $test->assertTrue( $wikiPage->isRedirect() );
1740 $test->assertSame( 99, $wikiPage->getLatest() );
1741 $test->assertSame( 3, $wikiPage->getTitle()->getNamespace() );
1742 $test->assertSame( 'JaJaTitle', $wikiPage->getTitle()->getDBkey() );
1745 'edit' => [ 'autoconfirmed', 'sysop' ],
1746 'move' => [ 'sysop' ],
1748 $wikiPage->getTitle()->getAllRestrictions()
1750 $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1751 $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1754 yield 'different timestamp formats' => [
1756 'page_touched' => '2012-01-01 02:02:02',
1757 'page_links_updated' => '2014-01-01 02:02:02',
1759 function ( WikiPage $wikiPage, self $test ) {
1760 $test->assertSame( '20120101020202', $wikiPage->getTouched() );
1761 $test->assertSame( '20140101020202', $wikiPage->getLinksTimestamp() );
1764 yield 'no restrictions' => [
1766 'page_restrictions' => '',
1768 function ( WikiPage $wikiPage, self $test ) {
1774 $wikiPage->getTitle()->getAllRestrictions()
1778 yield 'not redirect' => [
1780 'page_is_redirect' => '0',
1782 function ( WikiPage $wikiPage, self $test ) {
1783 $test->assertFalse( $wikiPage->isRedirect() );
1796 public function testNewFromRow( $row, $assertions ) {
1797 $page = WikiPage::newFromRow( $row, 'fromdb' );
1798 $assertions( $page, $this );
1801 public function provideTestNewFromId_returnsNullOnBadPageId() {
1810 public function testNewFromId_returnsNullOnBadPageId( $pageId ) {
1811 $this->assertNull( WikiPage::newFromID( $pageId ) );
1817 public function testNewFromId_appearsToFetchCorrectRow() {
1818 $createdPage = $this->createPage( __METHOD__, 'Xsfaij09' );
1819 $fetchedPage = WikiPage::newFromID( $createdPage->getId() );
1820 $this->assertSame( $createdPage->getId(), $fetchedPage->getId() );
1821 $this->assertEquals(
1822 $createdPage->getContent()->getText(),
1823 $fetchedPage->getContent()->getText()
1830 public function testNewFromId_returnsNullOnNonExistingId() {
1831 $this->assertNull( WikiPage::newFromID( 2147483647 ) );
1834 public function provideTestInsertProtectNullRevision() {
1835 // phpcs:disable Generic.Files.LineLength
1838 [ 'edit' => 'sysop' ],
1839 [ 'edit' => '20200101040404' ],
1843 '(goat-message-key: WikiPageDbTestBase::testInsertProtectNullRevision, UTSysop)(colon-separator)Goat Reason(word-separator)(parentheses: (protect-summary-desc: (restriction-edit), (protect-level-sysop), (protect-expiring: 04:04, 1 (january) 2020, 1 (january) 2020, 04:04)))'
1847 [ 'edit' => 'sysop', 'move' => 'something' ],
1848 [ 'edit' => '20200101040404', 'move' => '20210101050505' ],
1852 '(goat-key: WikiPageDbTestBase::testInsertProtectNullRevision, UTSysop)(colon-separator)Goat Goat(word-separator)(parentheses: (protect-summary-desc: (restriction-edit), (protect-level-sysop), (protect-expiring: 04:04, 1 (january) 2020, 1 (january) 2020, 04:04))(word-separator)(protect-summary-desc: (restriction-move), (protect-level-something), (protect-expiring: 05:05, 1 (january) 2021, 1 (january) 2021, 05:05)))'
1870 public function testInsertProtectNullRevision(
1879 $this->setContentLang( 'qqx' );
1881 $page = $this->createPage( __METHOD__, 'Goat' );
1883 $user = $user === null ? $user : $this->getTestSysop()->getUser();
1885 $result = $page->insertProtectNullRevision(
1894 $this->assertTrue( $result instanceof Revision );
1895 $this->assertSame( $expectedComment, $result->getComment( Revision::RAW ) );
1901 public function testUpdateRevisionOn_existingPage() {
1902 $user = $this->getTestSysop()->getUser();
1903 $page = $this->createPage( __METHOD__, 'StartText' );
1905 $revision = new Revision(
1908 'page' => $page->getId(),
1909 'title' => $page->getTitle(),
1910 'comment' => __METHOD__,
1911 'minor_edit' => true,
1912 'text' => __METHOD__ . '-text',
1913 'len' => strlen( __METHOD__ . '-text' ),
1914 'user' => $user->getId(),
1915 'user_text' => $user->getName(),
1916 'timestamp' => '20170707040404',
1917 'content_model' => CONTENT_MODEL_WIKITEXT,
1918 'content_format' => CONTENT_FORMAT_WIKITEXT,
1922 $result = $page->updateRevisionOn( $this->db, $revision );
1923 $this->assertTrue( $result );
1924 $this->assertSame( 9989, $page->getLatest() );
1925 $this->assertEquals( $revision, $page->getRevision() );
1931 public function testUpdateRevisionOn_NonExistingPage() {
1932 $user = $this->getTestSysop()->getUser();
1933 $page = $this->createPage( __METHOD__, 'StartText' );
1934 $page->doDeleteArticle( 'reason' );
1936 $revision = new Revision(
1939 'page' => $page->getId(),
1940 'title' => $page->getTitle(),
1941 'comment' => __METHOD__,
1942 'minor_edit' => true,
1943 'text' => __METHOD__ . '-text',
1944 'len' => strlen( __METHOD__ . '-text' ),
1945 'user' => $user->getId(),
1946 'user_text' => $user->getName(),
1947 'timestamp' => '20170707040404',
1948 'content_model' => CONTENT_MODEL_WIKITEXT,
1949 'content_format' => CONTENT_FORMAT_WIKITEXT,
1953 $result = $page->updateRevisionOn( $this->db, $revision );
1954 $this->assertFalse( $result );
1960 public function testUpdateIfNewerOn_olderRevision() {
1961 $user = $this->getTestSysop()->getUser();
1962 $page = $this->createPage( __METHOD__, 'StartText' );
1963 $initialRevision = $page->getRevision();
1965 $olderTimeStamp = wfTimestamp(
1967 wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) - 1
1970 $olderRevision = new Revision(
1973 'page' => $page->getId(),
1974 'title' => $page->getTitle(),
1975 'comment' => __METHOD__,
1976 'minor_edit' => true,
1977 'text' => __METHOD__ . '-text',
1978 'len' => strlen( __METHOD__ . '-text' ),
1979 'user' => $user->getId(),
1980 'user_text' => $user->getName(),
1981 'timestamp' => $olderTimeStamp,
1982 'content_model' => CONTENT_MODEL_WIKITEXT,
1983 'content_format' => CONTENT_FORMAT_WIKITEXT,
1987 $result = $page->updateIfNewerOn( $this->db, $olderRevision );
1988 $this->assertFalse( $result );
1994 public function testUpdateIfNewerOn_newerRevision() {
1995 $user = $this->getTestSysop()->getUser();
1996 $page = $this->createPage( __METHOD__, 'StartText' );
1997 $initialRevision = $page->getRevision();
1999 $newerTimeStamp = wfTimestamp(
2001 wfTimestamp( TS_UNIX, $initialRevision->getTimestamp() ) + 1
2004 $newerRevision = new Revision(
2007 'page' => $page->getId(),
2008 'title' => $page->getTitle(),
2009 'comment' => __METHOD__,
2010 'minor_edit' => true,
2011 'text' => __METHOD__ . '-text',
2012 'len' => strlen( __METHOD__ . '-text' ),
2013 'user' => $user->getId(),
2014 'user_text' => $user->getName(),
2015 'timestamp' => $newerTimeStamp,
2016 'content_model' => CONTENT_MODEL_WIKITEXT,
2017 'content_format' => CONTENT_FORMAT_WIKITEXT,
2020 $result = $page->updateIfNewerOn( $this->db, $newerRevision );
2021 $this->assertTrue( $result );
2027 public function testInsertOn() {
2028 $title = Title::newFromText( __METHOD__ );
2029 $page = new WikiPage( $title );
2031 $startTimeStamp = wfTimestampNow();
2032 $result = $page->insertOn( $this->db );
2033 $endTimeStamp = wfTimestampNow();
2035 $this->assertInternalType( 'int', $result );
2036 $this->assertTrue( $result > 0 );
2038 $condition = [ 'page_id' => $result ];
2040 // Check the default fields have been filled
2041 $this->assertSelect(
2046 'page_restrictions',
2064 // Check the page_random field has been filled
2065 $pageRandom = $this->db->selectField( 'page', 'page_random', $condition );
2066 $this->assertTrue( (float)$pageRandom < 1 && (float)$pageRandom > 0 );
2068 // Assert the touched timestamp in the DB is roughly when we inserted the page
2069 $pageTouched = $this->db->selectField( 'page', 'page_touched', $condition );
2071 wfTimestamp( TS_UNIX, $startTimeStamp )
2072 <= wfTimestamp( TS_UNIX, $pageTouched )
2075 wfTimestamp( TS_UNIX, $endTimeStamp )
2076 >= wfTimestamp( TS_UNIX, $pageTouched )
2079 // Try inserting the same page again and checking the result is false (no change)
2080 $result = $page->insertOn( $this->db );
2081 $this->assertFalse( $result );
2087 public function testInsertOn_idSpecified() {
2088 $title = Title::newFromText( __METHOD__ );
2089 $page = new WikiPage( $title );
2092 $result = $page->insertOn( $this->db, $id );
2094 $this->assertSame( $id, $result );
2096 $condition = [ 'page_id' => $result ];
2098 // Check there is actually a row in the db
2099 $this->assertSelect(
2107 public function provideTestDoUpdateRestrictions_setBasicRestrictions() {
2108 // Note: Once the current dates passes the date in these tests they will fail.
2109 yield 'move something' => [
2111 [ 'move' => 'something' ],
2113 [ 'edit' => [], 'move' => [ 'something' ] ],
2116 yield 'move something, edit blank' => [
2118 [ 'move' => 'something', 'edit' => '' ],
2120 [ 'edit' => [], 'move' => [ 'something' ] ],
2123 yield 'edit sysop, with expiry' => [
2125 [ 'edit' => 'sysop' ],
2126 [ 'edit' => '21330101020202' ],
2127 [ 'edit' => [ 'sysop' ], 'move' => [] ],
2128 [ 'edit' => '21330101020202' ],
2130 yield 'move and edit, move with expiry' => [
2132 [ 'move' => 'something', 'edit' => 'another' ],
2133 [ 'move' => '22220202010101' ],
2134 [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2135 [ 'move' => '22220202010101' ],
2137 yield 'move and edit, edit with infinity expiry' => [
2139 [ 'move' => 'something', 'edit' => 'another' ],
2140 [ 'edit' => 'infinity' ],
2141 [ 'edit' => [ 'another' ], 'move' => [ 'something' ] ],
2142 [ 'edit' => 'infinity' ],
2144 yield 'non existing, create something' => [
2146 [ 'create' => 'something' ],
2148 [ 'create' => [ 'something' ] ],
2151 yield 'non existing, create something with expiry' => [
2153 [ 'create' => 'something' ],
2154 [ 'create' => '23451212112233' ],
2155 [ 'create' => [ 'something' ] ],
2156 [ 'create' => '23451212112233' ],
2164 public function testDoUpdateRestrictions_setBasicRestrictions(
2168 array $expectedRestrictions,
2169 array $expectedRestrictionExpiries
2171 if ( $pageExists ) {
2172 $page = $this->createPage( __METHOD__, 'ABC' );
2174 $page = new WikiPage( Title::newFromText( __METHOD__ . '-nonexist' ) );
2176 $user = $this->getTestSysop()->getUser();
2179 $status = $page->doUpdateRestrictions( $limit, $expiry, $cascade, 'aReason', $user, [] );
2181 $logId = $status->getValue();
2182 $allRestrictions = $page->getTitle()->getAllRestrictions();
2184 $this->assertTrue( $status->isGood() );
2185 $this->assertInternalType( 'int', $logId );
2186 $this->assertSame( $expectedRestrictions, $allRestrictions );
2187 foreach ( $expectedRestrictionExpiries as $key => $value ) {
2188 $this->assertSame( $value, $page->getTitle()->getRestrictionExpiry( $key ) );
2191 // Make sure the log entry looks good
2192 // log_params is not checked here
2193 $actorQuery = ActorMigration::newMigration()->getJoin( 'log_user' );
2194 $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'log_comment' );
2195 $this->assertSelect(
2196 [ 'logging' ] + $actorQuery['tables'] + $commentQuery['tables'],
2198 'log_comment' => $commentQuery['fields']['log_comment_text'],
2199 'log_user' => $actorQuery['fields']['log_user'],
2200 'log_user_text' => $actorQuery['fields']['log_user_text'],
2204 [ 'log_id' => $logId ],
2207 (string)$user->getId(),
2209 (string)$page->getTitle()->getNamespace(),
2210 $page->getTitle()->getDBkey(),
2213 $actorQuery['joins'] + $commentQuery['joins']
2220 public function testDoUpdateRestrictions_failsOnReadOnly() {
2221 $page = $this->createPage( __METHOD__, 'ABC' );
2222 $user = $this->getTestSysop()->getUser();
2226 $readOnly = $this->getMockBuilder( ReadOnlyMode::class )
2227 ->disableOriginalConstructor()
2228 ->setMethods( [ 'isReadOnly', 'getReason' ] )
2230 $readOnly->expects( $this->once() )
2231 ->method( 'isReadOnly' )
2232 ->will( $this->returnValue( true ) );
2233 $readOnly->expects( $this->once() )
2234 ->method( 'getReason' )
2235 ->will( $this->returnValue( 'Some Read Only Reason' ) );
2236 $this->setService( 'ReadOnlyMode', $readOnly );
2238 $status = $page->doUpdateRestrictions( [], [], $cascade, 'aReason', $user, [] );
2239 $this->assertFalse( $status->isOK() );
2240 $this->assertSame( 'readonlytext', $status->getMessage()->getKey() );
2246 public function testDoUpdateRestrictions_returnsGoodIfNothingChanged() {
2247 $page = $this->createPage( __METHOD__, 'ABC' );
2248 $user = $this->getTestSysop()->getUser();
2250 $limit = [ 'edit' => 'sysop' ];
2252 $status = $page->doUpdateRestrictions(
2261 // The first entry should have a logId as it did something
2262 $this->assertTrue( $status->isGood() );
2263 $this->assertInternalType( 'int', $status->getValue() );
2265 $status = $page->doUpdateRestrictions(
2274 // The second entry should not have a logId as nothing changed
2275 $this->assertTrue( $status->isGood() );
2276 $this->assertNull( $status->getValue() );
2282 public function testDoUpdateRestrictions_logEntryTypeAndAction() {
2283 $page = $this->createPage( __METHOD__, 'ABC' );
2284 $user = $this->getTestSysop()->getUser();
2288 $status = $page->doUpdateRestrictions(
2289 [ 'edit' => 'sysop' ],
2296 $this->assertTrue( $status->isGood() );
2297 $this->assertInternalType( 'int', $status->getValue() );
2298 $this->assertSelect(
2300 [ 'log_type', 'log_action' ],
2301 [ 'log_id' => $status->getValue() ],
2302 [ [ 'protect', 'protect' ] ]
2305 // Modify the protection
2306 $status = $page->doUpdateRestrictions(
2307 [ 'edit' => 'somethingElse' ],
2314 $this->assertTrue( $status->isGood() );
2315 $this->assertInternalType( 'int', $status->getValue() );
2316 $this->assertSelect(
2318 [ 'log_type', 'log_action' ],
2319 [ 'log_id' => $status->getValue() ],
2320 [ [ 'protect', 'modify' ] ]
2323 // Remove the protection
2324 $status = $page->doUpdateRestrictions(
2332 $this->assertTrue( $status->isGood() );
2333 $this->assertInternalType( 'int', $status->getValue() );
2334 $this->assertSelect(
2336 [ 'log_type', 'log_action' ],
2337 [ 'log_id' => $status->getValue() ],
2338 [ [ 'protect', 'unprotect' ] ]
2346 public function testNewPageUpdater() {
2347 $user = $this->getTestUser()->getUser();
2348 $page = $this->newPage( __METHOD__, __METHOD__ );
2351 $content = $this->getMockBuilder( WikitextContent::class )
2352 ->setConstructorArgs( [ 'Hello World' ] )
2353 ->setMethods( [ 'getParserOutput' ] )
2355 $content->expects( $this->once() )
2356 ->method( 'getParserOutput' )
2357 ->willReturn( new ParserOutput( 'HTML' ) );
2359 $preparedEditBefore = $page->prepareContentForEdit( $content, null, $user );
2361 // provide context, so the cache can be kept in place
2362 $slotsUpdate = new revisionSlotsUpdate();
2363 $slotsUpdate->modifyContent( SlotRecord::MAIN, $content );
2365 $updater = $page->newPageUpdater( $user, $slotsUpdate );
2366 $updater->setContent( SlotRecord::MAIN, $content );
2367 $revision = $updater->saveRevision(
2368 CommentStoreComment::newUnsavedComment( 'test' ),
2372 $preparedEditAfter = $page->prepareContentForEdit( $content, $revision, $user );
2374 $this->assertSame( $revision->getId(), $page->getLatest() );
2376 // Parsed output must remain cached throughout.
2377 $this->assertSame( $preparedEditBefore->output, $preparedEditAfter->output );
2384 public function testGetDerivedDataUpdater() {
2385 $admin = $this->getTestSysop()->getUser();
2388 $page = $this->createPage( __METHOD__, __METHOD__ );
2389 $page = TestingAccessWrapper::newFromObject( $page );
2391 $revision = $page->getRevision()->getRevisionRecord();
2392 $user = $revision->getUser();
2394 $slotsUpdate = new RevisionSlotsUpdate();
2395 $slotsUpdate->modifyContent( SlotRecord::MAIN, new WikitextContent( 'Hello World' ) );
2397 // get a virgin updater
2398 $updater1 = $page->getDerivedDataUpdater( $user );
2399 $this->assertFalse( $updater1->isUpdatePrepared() );
2401 $updater1->prepareUpdate( $revision );
2403 // Re-use updater with same revision or content, even if base changed
2404 $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, $revision ) );
2406 $slotsUpdate = RevisionSlotsUpdate::newFromContent(
2407 [ SlotRecord::MAIN => $revision->getContent( SlotRecord::MAIN ) ]
2409 $this->assertSame( $updater1, $page->getDerivedDataUpdater( $user, null, $slotsUpdate ) );
2411 // Don't re-use for edit if base revision ID changed
2412 $this->assertNotSame(
2414 $page->getDerivedDataUpdater( $user, null, $slotsUpdate, true )
2417 // Don't re-use with different user
2418 $updater2a = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2419 $updater2a->prepareContent( $admin, $slotsUpdate, false );
2421 $updater2b = $page->getDerivedDataUpdater( $user, null, $slotsUpdate );
2422 $updater2b->prepareContent( $user, $slotsUpdate, false );
2423 $this->assertNotSame( $updater2a, $updater2b );
2425 // Don't re-use with different content
2426 $updater3 = $page->getDerivedDataUpdater( $admin, null, $slotsUpdate );
2427 $updater3->prepareUpdate( $revision );
2428 $this->assertNotSame( $updater2b, $updater3 );
2430 // Don't re-use if no context given
2431 $updater4 = $page->getDerivedDataUpdater( $admin );
2432 $updater4->prepareUpdate( $revision );
2433 $this->assertNotSame( $updater3, $updater4 );
2435 // Don't re-use if AGAIN no context given
2436 $updater5 = $page->getDerivedDataUpdater( $admin );
2437 $this->assertNotSame( $updater4, $updater5 );
2439 // Don't re-use cached "virgin
" unprepared updater
2440 $updater6 = $page->getDerivedDataUpdater( $admin, $revision );
2441 $this->assertNotSame( $updater5, $updater6 );
2444 protected function assertPreparedEditEquals(
2445 PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2447 // suppress differences caused by a clock tick between generating the two PreparedEdits
2448 if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2449 $edit2 = clone $edit2;
2450 $edit2->timestamp = $edit->timestamp;
2452 $this->assertEquals( $edit, $edit2, $message );
2455 protected function assertPreparedEditNotEquals(
2456 PreparedEdit $edit, PreparedEdit $edit2, $message = ''
2458 if ( abs( $edit->timestamp - $edit2->timestamp ) < 3 ) {
2459 $edit2 = clone $edit2;
2460 $edit2->timestamp = $edit->timestamp;
2462 $this->assertNotEquals( $edit, $edit2, $message );
could not be made into a sysop(Did you enter the name correctly?) <
and that you know you can do these things To protect your we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights These restrictions translate to certain responsibilities for you if you distribute copies of the or if you modify it For if you distribute copies of such a whether gratis or for a you must give the recipients all the rights that you have You must make sure that receive or can get the source code And you must show them these terms so they know their rights We protect your rights with two and(2) offer you this license which gives you legal permission to copy
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
A content handler knows how do deal with a specific type of content on a wiki page.
Deferrable Update for closure/callback.
loadParamsAndArgs( $self=null, $opts=null, $args=null)
Process command line arguments $mOptions becomes an array with keys set to the option names $mArgs be...
Maintenance script that runs pending jobs.
Represents a title within MediaWiki.
testDoEditContent_twice()
WikiPage::doEditContent.
testGetRedirectTarget( $title, $model, $text, $target)
provideGetRedirectTarget WikiPage::getRedirectTarget
testGetContent()
WikiPage::getContent.
newPage( $title, $model=null)
testPrepareContentForEdit()
WikiPage::prepareContentForEdit.
testExists()
WikiPage::exists.
assertPreparedEditNotEquals(PreparedEdit $edit, PreparedEdit $edit2, $message='')
testHasViewableContent( $title, $viewable, $create=false)
provideHasViewableContent WikiPage::hasViewableContent
testDoEditContent()
WikiPage::doEditContent WikiPage::prepareContentForEdit.
createPage( $page, $content, $model=null, $user=null)
assertPreparedEditEquals(PreparedEdit $edit, PreparedEdit $edit2, $message='')
defineMockContentModelForUpdateTesting( $name)
testGetRevision()
WikiPage::getRevision.
provideHasViewableContent()
testDoDeleteArticleReal_user0()
WikiPage::doDeleteArticleReal.
provideGetRedirectTarget()
testIsCountable( $title, $model, $text, $mode, $expected)
provideIsCountable WikiPage::isCountable
testDoDeleteArticle()
Undeletion is covered in PageArchiveTest::testUndeleteRevisions() TODO: Revision deletion.
testDoDeleteArticleReal_suppress()
TODO: Test more stuff about suppression.
testDoEditUpdates()
WikiPage::doEditUpdates.
testDoDeleteArticleReal_userSysop()
WikiPage::doDeleteArticleReal.
testIsRedirect( $title, $model, $text, $target)
provideGetRedirectTarget WikiPage::isRedirect
testDoDeleteUpdates()
WikiPage::doDeleteUpdates.
__construct( $name=null, array $data=[], $dataName='')
createMockContent(ContentHandler $handler, $text)
testGetParserOutput( $model, $text, $expectedHtml)
provideGetParserOutput WikiPage::getParserOutput
Class representing a MediaWiki article and history.
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
presenting them properly to the user as errors is done by the caller return true use this to change the list i e rollback
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as
patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
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
processing should stop and the error should be shown to the user * false
$data
Utility to generate mapping file used in mw.Title (phpCharToUpper.json)
const CONTENT_MODEL_WIKITEXT
const CONTENT_FORMAT_WIKITEXT
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$page->newPageUpdater($user) $updater