setMwGlobals( [ 'wgExtraNamespaces' => $wgExtraNamespaces, 'wgNamespaceContentModels' => $wgNamespaceContentModels, 'wgContentHandlers' => $wgContentHandlers, 'wgContLang' => $wgContLang, ] ); $wgExtraNamespaces[12312] = 'Dummy'; $wgExtraNamespaces[12313] = 'Dummy_talk'; $wgNamespaceContentModels[12312] = "testing"; $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting'; MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); # reset namespace cache } protected function tearDown() { global $wgContLang; MWNamespace::clearCaches(); $wgContLang->resetNamespaces(); # reset namespace cache parent::tearDown(); } /** * @dataProvider provideExtractSectionTitle * @covers EditPage::extractSectionTitle */ public function testExtractSectionTitle( $section, $title ) { $extracted = EditPage::extractSectionTitle( $section ); $this->assertEquals( $title, $extracted ); } public static function provideExtractSectionTitle() { return [ [ "== Test ==\n\nJust a test section.", "Test" ], [ "An initial section, no header.", false ], [ "An initial section with a fake heder (T34617)\n\n== Test == ??\nwtf", false ], [ "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo", "Section" ], [ "== Section== \t\r\n followed by whitespace (T37051)", 'Section', ], ]; } protected function forceRevisionDate( WikiPage $page, $timestamp ) { $dbw = wfGetDB( DB_MASTER ); $dbw->update( 'revision', [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ], [ 'rev_id' => $page->getLatest() ] ); $page->clear(); } /** * User input text is passed to rtrim() by edit page. This is a simple * wrapper around assertEquals() which calls rrtrim() to normalize the * expected and actual texts. * @param string $expected * @param string $actual * @param string $msg */ protected function assertEditedTextEquals( $expected, $actual, $msg = '' ) { $this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg ); } /** * Performs an edit and checks the result. * * @param string|Title $title The title of the page to edit * @param string|null $baseText Some text to create the page with before attempting the edit. * @param User|string|null $user The user to perform the edit as. * @param array $edit An array of request parameters used to define the edit to perform. * Some well known fields are: * * wpTextbox1: the text to submit * * wpSummary: the edit summary * * wpEditToken: the edit token (will be inserted if not provided) * * wpEdittime: timestamp of the edit's base revision (will be inserted * if not provided) * * wpStarttime: timestamp when the edit started (will be inserted if not provided) * * wpSectionTitle: the section to edit * * wpMinorEdit: mark as minor edit * * wpWatchthis: whether to watch the page * @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants). * Set to null to skip the check. * @param string|null $expectedText The text expected to be on the page after the edit. * Set to null to skip the check. * @param string|null $message An optional message to show along with any error message. * * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc. */ protected function assertEdit( $title, $baseText, $user = null, array $edit, $expectedCode = null, $expectedText = null, $message = null ) { if ( is_string( $title ) ) { $ns = $this->getDefaultWikitextNS(); $title = Title::newFromText( $title, $ns ); } $this->assertNotNull( $title ); if ( is_string( $user ) ) { $user = User::newFromName( $user ); if ( $user->getId() === 0 ) { $user->addToDatabase(); } } $page = WikiPage::factory( $title ); if ( $baseText !== null ) { $content = ContentHandler::makeContent( $baseText, $title ); $page->doEditContent( $content, "base text for test" ); $this->forceRevisionDate( $page, '20120101000000' ); // sanity check $page->clear(); $currentText = ContentHandler::getContentText( $page->getContent() ); # EditPage rtrim() the user input, so we alter our expected text # to reflect that. $this->assertEditedTextEquals( $baseText, $currentText ); } if ( $user == null ) { $user = $GLOBALS['wgUser']; } else { $this->setMwGlobals( 'wgUser', $user ); } if ( !isset( $edit['wpEditToken'] ) ) { $edit['wpEditToken'] = $user->getEditToken(); } if ( !isset( $edit['wpEdittime'] ) ) { $edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : ''; } if ( !isset( $edit['wpStarttime'] ) ) { $edit['wpStarttime'] = wfTimestampNow(); } if ( !isset( $edit['wpUnicodeCheck'] ) ) { $edit['wpUnicodeCheck'] = EditPage::UNICODE_CHECK; } $req = new FauxRequest( $edit, true ); // session ?? $article = new Article( $title ); $article->getContext()->setTitle( $title ); $ep = new EditPage( $article ); $ep->setContextTitle( $title ); $ep->importFormData( $req ); $bot = isset( $edit['bot'] ) ? (bool)$edit['bot'] : false; // this is where the edit happens! // Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut // and throws exceptions like PermissionsError $status = $ep->internalAttemptSave( $result, $bot ); if ( $expectedCode !== null ) { // check edit code $this->assertEquals( $expectedCode, $status->value, "Expected result code mismatch. $message" ); } $page = WikiPage::factory( $title ); if ( $expectedText !== null ) { // check resulting page text $content = $page->getContent(); $text = ContentHandler::getContentText( $content ); # EditPage rtrim() the user input, so we alter our expected text # to reflect that. $this->assertEditedTextEquals( $expectedText, $text, "Expected article text mismatch. $message" ); } return $page; } public static function provideCreatePages() { return [ [ 'expected article being created', 'EditPageTest_testCreatePage', null, 'Hello World!', EditPage::AS_SUCCESS_NEW_ARTICLE, 'Hello World!' ], [ 'expected article not being created if empty', 'EditPageTest_testCreatePage', null, '', EditPage::AS_BLANK_ARTICLE, null ], [ 'expected MediaWiki: page being created', 'MediaWiki:January', 'UTSysop', 'Not January', EditPage::AS_SUCCESS_NEW_ARTICLE, 'Not January' ], [ 'expected not-registered MediaWiki: page not being created if empty', 'MediaWiki:EditPageTest_testCreatePage', 'UTSysop', '', EditPage::AS_BLANK_ARTICLE, null ], [ 'expected registered MediaWiki: page being created even if empty', 'MediaWiki:January', 'UTSysop', '', EditPage::AS_SUCCESS_NEW_ARTICLE, '' ], [ 'expected registered MediaWiki: page whose default content is empty' . ' not being created if empty', 'MediaWiki:Ipb-default-expiry', 'UTSysop', '', EditPage::AS_BLANK_ARTICLE, '' ], [ 'expected MediaWiki: page not being created if text equals default message', 'MediaWiki:January', 'UTSysop', 'January', EditPage::AS_BLANK_ARTICLE, null ], [ 'expected empty article being created', 'EditPageTest_testCreatePage', null, '', EditPage::AS_SUCCESS_NEW_ARTICLE, '', true ], ]; } /** * @dataProvider provideCreatePages * @covers EditPage */ public function testCreatePage( $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false ) { $checkId = null; $this->setMwGlobals( 'wgHooks', [ 'PageContentInsertComplete' => [ function ( WikiPage &$page, User &$user, Content $content, $summary, $minor, $u1, $u2, &$flags, Revision $revision ) { // types/refs checked } ], 'PageContentSaveComplete' => [ function ( WikiPage &$page, User &$user, Content $content, $summary, $minor, $u1, $u2, &$flags, Revision $revision, Status &$status, $baseRevId ) use ( &$checkId ) { $checkId = $status->value['revision']->getId(); // types/refs checked } ], ] ); $edit = [ 'wpTextbox1' => $editText ]; if ( $ignoreBlank ) { $edit['wpIgnoreBlankArticle'] = 1; } $page = $this->assertEdit( $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc ); if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) { $latest = $page->getLatest(); $page->doDeleteArticleReal( $pageTitle ); $this->assertGreaterThan( 0, $latest, "Page revision ID updated in object" ); $this->assertEquals( $latest, $checkId, "Revision in Status for hook" ); } } /** * @dataProvider provideCreatePages * @covers EditPage */ public function testCreatePageTrx( $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false ) { $checkIds = []; $this->setMwGlobals( 'wgHooks', [ 'PageContentInsertComplete' => [ function ( WikiPage &$page, User &$user, Content $content, $summary, $minor, $u1, $u2, &$flags, Revision $revision ) { // types/refs checked } ], 'PageContentSaveComplete' => [ function ( WikiPage &$page, User &$user, Content $content, $summary, $minor, $u1, $u2, &$flags, Revision $revision, Status &$status, $baseRevId ) use ( &$checkIds ) { $checkIds[] = $status->value['revision']->getId(); // types/refs checked } ], ] ); wfGetDB( DB_MASTER )->begin( __METHOD__ ); $edit = [ 'wpTextbox1' => $editText ]; if ( $ignoreBlank ) { $edit['wpIgnoreBlankArticle'] = 1; } $page = $this->assertEdit( $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc ); $pageTitle2 = (string)$pageTitle . '/x'; $page2 = $this->assertEdit( $pageTitle2, null, $user, $edit, $expectedCode, $expectedText, $desc ); wfGetDB( DB_MASTER )->commit( __METHOD__ ); $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount(), 'No deferred updates' ); if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) { $latest = $page->getLatest(); $page->doDeleteArticleReal( $pageTitle ); $this->assertGreaterThan( 0, $latest, "Page #1 revision ID updated in object" ); $this->assertEquals( $latest, $checkIds[0], "Revision #1 in Status for hook" ); $latest2 = $page2->getLatest(); $page2->doDeleteArticleReal( $pageTitle2 ); $this->assertGreaterThan( 0, $latest2, "Page #2 revision ID updated in object" ); $this->assertEquals( $latest2, $checkIds[1], "Revision #2 in Status for hook" ); } } public function testUpdatePage() { $checkIds = []; $this->setMwGlobals( 'wgHooks', [ 'PageContentInsertComplete' => [ function ( WikiPage &$page, User &$user, Content $content, $summary, $minor, $u1, $u2, &$flags, Revision $revision ) { // types/refs checked } ], 'PageContentSaveComplete' => [ function ( WikiPage &$page, User &$user, Content $content, $summary, $minor, $u1, $u2, &$flags, Revision $revision, Status &$status, $baseRevId ) use ( &$checkIds ) { $checkIds[] = $status->value['revision']->getId(); // types/refs checked } ], ] ); $text = "one"; $edit = [ 'wpTextbox1' => $text, 'wpSummary' => 'first update', ]; $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit, EditPage::AS_SUCCESS_UPDATE, $text, "expected successfull update with given text" ); $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" ); $this->forceRevisionDate( $page, '20120101000000' ); $text = "two"; $edit = [ 'wpTextbox1' => $text, 'wpSummary' => 'second update', ]; $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit, EditPage::AS_SUCCESS_UPDATE, $text, "expected successfull update with given text" ); $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" ); $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" ); } public function testUpdatePageTrx() { $text = "one"; $edit = [ 'wpTextbox1' => $text, 'wpSummary' => 'first update', ]; $page = $this->assertEdit( 'EditPageTest_testTrxUpdatePage', "zero", null, $edit, EditPage::AS_SUCCESS_UPDATE, $text, "expected successfull update with given text" ); $this->forceRevisionDate( $page, '20120101000000' ); $checkIds = []; $this->setMwGlobals( 'wgHooks', [ 'PageContentSaveComplete' => [ function ( WikiPage &$page, User &$user, Content $content, $summary, $minor, $u1, $u2, &$flags, Revision $revision, Status &$status, $baseRevId ) use ( &$checkIds ) { $checkIds[] = $status->value['revision']->getId(); // types/refs checked } ], ] ); wfGetDB( DB_MASTER )->begin( __METHOD__ ); $text = "two"; $edit = [ 'wpTextbox1' => $text, 'wpSummary' => 'second update', ]; $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit, EditPage::AS_SUCCESS_UPDATE, $text, "expected successfull update with given text" ); $text = "three"; $edit = [ 'wpTextbox1' => $text, 'wpSummary' => 'third update', ]; $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit, EditPage::AS_SUCCESS_UPDATE, $text, "expected successfull update with given text" ); wfGetDB( DB_MASTER )->commit( __METHOD__ ); $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" ); $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" ); $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" ); } public static function provideSectionEdit() { $text = 'Intro == one == first section. == two == second section. '; $sectionOne = '== one == hello '; $newSection = '== new section == hello '; $textWithNewSectionOne = preg_replace( '/== one ==.*== two ==/ms', "$sectionOne\n== two ==", $text ); $textWithNewSectionAdded = "$text\n$newSection"; return [ [ # 0 $text, '', 'hello', 'replace all', 'hello' ], [ # 1 $text, '1', $sectionOne, 'replace first section', $textWithNewSectionOne, ], [ # 2 $text, 'new', 'hello', 'new section', $textWithNewSectionAdded, ], ]; } /** * @dataProvider provideSectionEdit * @covers EditPage */ public function testSectionEdit( $base, $section, $text, $summary, $expected ) { $edit = [ 'wpTextbox1' => $text, 'wpSummary' => $summary, 'wpSection' => $section, ]; $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit, EditPage::AS_SUCCESS_UPDATE, $expected, "expected successfull update of section" ); } public static function provideAutoMerge() { $tests = []; $tests[] = [ # 0: plain conflict "Elmo", # base edit user "one\n\ntwo\n\nthree\n", [ # adam's edit 'wpStarttime' => 1, 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n", ], [ # berta's edit 'wpStarttime' => 2, 'wpTextbox1' => "(one)\n\ntwo\n\nthree\n", ], EditPage::AS_CONFLICT_DETECTED, # expected code "ONE\n\ntwo\n\nthree\n", # expected text 'expected edit conflict', # message ]; $tests[] = [ # 1: successful merge "Elmo", # base edit user "one\n\ntwo\n\nthree\n", [ # adam's edit 'wpStarttime' => 1, 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n", ], [ # berta's edit 'wpStarttime' => 2, 'wpTextbox1' => "one\n\ntwo\n\nTHREE\n", ], EditPage::AS_SUCCESS_UPDATE, # expected code "ONE\n\ntwo\n\nTHREE\n", # expected text 'expected automatic merge', # message ]; $text = "Intro\n\n"; $text .= "== first section ==\n\n"; $text .= "one\n\ntwo\n\nthree\n\n"; $text .= "== second section ==\n\n"; $text .= "four\n\nfive\n\nsix\n\n"; // extract the first section. $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text ); // generate expected text after merge $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) ); $tests[] = [ # 2: merge in section "Elmo", # base edit user $text, [ # adam's edit 'wpStarttime' => 1, 'wpTextbox1' => str_replace( 'one', 'ONE', $section ), 'wpSection' => '1' ], [ # berta's edit 'wpStarttime' => 2, 'wpTextbox1' => str_replace( 'three', 'THREE', $section ), 'wpSection' => '1' ], EditPage::AS_SUCCESS_UPDATE, # expected code $expected, # expected text 'expected automatic section merge', # message ]; // see whether it makes a difference who did the base edit $testsWithAdam = array_map( function ( $test ) { $test[0] = 'Adam'; // change base edit user return $test; }, $tests ); $testsWithBerta = array_map( function ( $test ) { $test[0] = 'Berta'; // change base edit user return $test; }, $tests ); return array_merge( $tests, $testsWithAdam, $testsWithBerta ); } /** * @dataProvider provideAutoMerge * @covers EditPage */ public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit, $expectedCode, $expectedText, $message = null ) { $this->markTestSkippedIfNoDiff3(); // create page $ns = $this->getDefaultWikitextNS(); $title = Title::newFromText( 'EditPageTest_testAutoMerge', $ns ); $page = WikiPage::factory( $title ); if ( $page->exists() ) { $page->doDeleteArticle( "clean slate for testing" ); } $baseEdit = [ 'wpTextbox1' => $text, ]; $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null, $baseUser, $baseEdit, null, null, __METHOD__ ); $this->forceRevisionDate( $page, '20120101000000' ); $edittime = $page->getTimestamp(); // start timestamps for conflict detection if ( !isset( $adamsEdit['wpStarttime'] ) ) { $adamsEdit['wpStarttime'] = 1; } if ( !isset( $bertasEdit['wpStarttime'] ) ) { $bertasEdit['wpStarttime'] = 2; } $starttime = wfTimestampNow(); $adamsTime = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$adamsEdit['wpStarttime'] ); $bertasTime = wfTimestamp( TS_MW, (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$bertasEdit['wpStarttime'] ); $adamsEdit['wpStarttime'] = $adamsTime; $bertasEdit['wpStarttime'] = $bertasTime; $adamsEdit['wpSummary'] = 'Adam\'s edit'; $bertasEdit['wpSummary'] = 'Bertas\'s edit'; $adamsEdit['wpEdittime'] = $edittime; $bertasEdit['wpEdittime'] = $edittime; // first edit $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit, EditPage::AS_SUCCESS_UPDATE, null, "expected successfull update" ); // second edit $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit, $expectedCode, $expectedText, $message ); } /** * @depends testAutoMerge */ public function testCheckDirectEditingDisallowed_forNonTextContent() { $title = Title::newFromText( 'Dummy:NonTextPageForEditPage' ); $page = WikiPage::factory( $title ); $article = new Article( $title ); $article->getContext()->setTitle( $title ); $ep = new EditPage( $article ); $ep->setContextTitle( $title ); $user = $GLOBALS['wgUser']; $edit = [ 'wpTextbox1' => serialize( 'non-text content' ), 'wpEditToken' => $user->getEditToken(), 'wpEdittime' => '', 'wpStarttime' => wfTimestampNow(), 'wpUnicodeCheck' => EditPage::UNICODE_CHECK, ]; $req = new FauxRequest( $edit, true ); $ep->importFormData( $req ); $this->setExpectedException( MWException::class, 'This content model is not supported: testing' ); $ep->internalAttemptSave( $result, false ); } }