summaryrefslogtreecommitdiff
path: root/www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php
diff options
context:
space:
mode:
Diffstat (limited to 'www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php')
-rw-r--r--www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php1604
1 files changed, 1604 insertions, 0 deletions
diff --git a/www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php b/www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php
new file mode 100644
index 00000000..c1963389
--- /dev/null
+++ b/www/wiki/tests/phpunit/includes/api/ApiEditPageTest.php
@@ -0,0 +1,1604 @@
+<?php
+
+/**
+ * Tests for MediaWiki api.php?action=edit.
+ *
+ * @author Daniel Kinzler
+ *
+ * @group API
+ * @group Database
+ * @group medium
+ *
+ * @covers ApiEditPage
+ */
+class ApiEditPageTest extends ApiTestCase {
+
+ protected function setUp() {
+ global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
+
+ parent::setUp();
+
+ $this->setMwGlobals( [
+ 'wgExtraNamespaces' => $wgExtraNamespaces,
+ 'wgNamespaceContentModels' => $wgNamespaceContentModels,
+ 'wgContentHandlers' => $wgContentHandlers,
+ 'wgContLang' => $wgContLang,
+ ] );
+
+ $wgExtraNamespaces[12312] = 'Dummy';
+ $wgExtraNamespaces[12313] = 'Dummy_talk';
+ $wgExtraNamespaces[12314] = 'DummyNonText';
+ $wgExtraNamespaces[12315] = 'DummyNonText_talk';
+
+ $wgNamespaceContentModels[12312] = "testing";
+ $wgNamespaceContentModels[12314] = "testing-nontext";
+
+ $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
+ $wgContentHandlers["testing-nontext"] = 'DummyNonTextContentHandler';
+ $wgContentHandlers["testing-serialize-error"] =
+ 'DummySerializeErrorContentHandler';
+
+ MWNamespace::clearCaches();
+ $wgContLang->resetNamespaces(); # reset namespace cache
+ }
+
+ protected function tearDown() {
+ global $wgContLang;
+
+ MWNamespace::clearCaches();
+ $wgContLang->resetNamespaces(); # reset namespace cache
+
+ parent::tearDown();
+ }
+
+ public function testEdit() {
+ $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext
+
+ // -- test new page --------------------------------------------
+ $apiResult = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ ] );
+ $apiResult = $apiResult[0];
+
+ // Validate API result data
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertSame( 'Success', $apiResult['edit']['result'] );
+
+ $this->assertArrayHasKey( 'new', $apiResult['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
+
+ $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
+
+ // -- test existing page, no change ----------------------------
+ $data = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ ] );
+
+ $this->assertSame( 'Success', $data[0]['edit']['result'] );
+
+ $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
+ $this->assertArrayHasKey( 'nochange', $data[0]['edit'] );
+
+ // -- test existing page, with change --------------------------
+ $data = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'different text'
+ ] );
+
+ $this->assertSame( 'Success', $data[0]['edit']['result'] );
+
+ $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] );
+
+ $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] );
+ $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] );
+ $this->assertNotEquals(
+ $data[0]['edit']['newrevid'],
+ $data[0]['edit']['oldrevid'],
+ "revision id should change after edit"
+ );
+ }
+
+ /**
+ * @return array
+ */
+ public static function provideEditAppend() {
+ return [
+ [ # 0: append
+ 'foo', 'append', 'bar', "foobar"
+ ],
+ [ # 1: prepend
+ 'foo', 'prepend', 'bar', "barfoo"
+ ],
+ [ # 2: append to empty page
+ '', 'append', 'foo', "foo"
+ ],
+ [ # 3: prepend to empty page
+ '', 'prepend', 'foo', "foo"
+ ],
+ [ # 4: append to non-existing page
+ null, 'append', 'foo', "foo"
+ ],
+ [ # 5: prepend to non-existing page
+ null, 'prepend', 'foo', "foo"
+ ],
+ ];
+ }
+
+ /**
+ * @dataProvider provideEditAppend
+ */
+ public function testEditAppend( $text, $op, $append, $expected ) {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditAppend_$count";
+
+ // -- create page (or not) -----------------------------------------
+ if ( $text !== null ) {
+ list( $re ) = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => $text, ] );
+
+ $this->assertSame( 'Success', $re['edit']['result'] ); // sanity
+ }
+
+ // -- try append/prepend --------------------------------------------
+ list( $re ) = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ $op . 'text' => $append, ] );
+
+ $this->assertSame( 'Success', $re['edit']['result'] );
+
+ // -- validate -----------------------------------------------------
+ $page = new WikiPage( Title::newFromText( $name ) );
+ $content = $page->getContent();
+ $this->assertNotNull( $content, 'Page should have been created' );
+
+ $text = $content->getNativeData();
+
+ $this->assertSame( $expected, $text );
+ }
+
+ /**
+ * Test editing of sections
+ */
+ public function testEditSection() {
+ $name = 'Help:ApiEditPageTest_testEditSection';
+ $page = WikiPage::factory( Title::newFromText( $name ) );
+ $text = "==section 1==\ncontent 1\n==section 2==\ncontent2";
+ // Preload the page with some text
+ $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), 'summary' );
+
+ list( $re ) = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => '1',
+ 'text' => "==section 1==\nnew content 1",
+ ] );
+ $this->assertSame( 'Success', $re['edit']['result'] );
+ $newtext = WikiPage::factory( Title::newFromText( $name ) )
+ ->getContent( Revision::RAW )
+ ->getNativeData();
+ $this->assertSame( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext );
+
+ // Test that we raise a 'nosuchsection' error
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => '9999',
+ 'text' => 'text',
+ ] );
+ $this->fail( "Should have raised an ApiUsageException" );
+ } catch ( ApiUsageException $e ) {
+ $this->assertTrue( self::apiExceptionHasCode( $e, 'nosuchsection' ) );
+ }
+ }
+
+ /**
+ * Test action=edit&section=new
+ * Run it twice so we test adding a new section on a
+ * page that doesn't exist (T54830) and one that
+ * does exist
+ */
+ public function testEditNewSection() {
+ $name = 'Help:ApiEditPageTest_testEditNewSection';
+
+ // Test on a page that does not already exist
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ list( $re ) = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => 'new',
+ 'text' => 'test',
+ 'summary' => 'header',
+ ] );
+
+ $this->assertSame( 'Success', $re['edit']['result'] );
+ // Check the page text is correct
+ $text = WikiPage::factory( Title::newFromText( $name ) )
+ ->getContent( Revision::RAW )
+ ->getNativeData();
+ $this->assertSame( "== header ==\n\ntest", $text );
+
+ // Now on one that does
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ list( $re2 ) = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'section' => 'new',
+ 'text' => 'test',
+ 'summary' => 'header',
+ ] );
+
+ $this->assertSame( 'Success', $re2['edit']['result'] );
+ $text = WikiPage::factory( Title::newFromText( $name ) )
+ ->getContent( Revision::RAW )
+ ->getNativeData();
+ $this->assertSame( "== header ==\n\ntest\n\n== header ==\n\ntest", $text );
+ }
+
+ /**
+ * Ensure we can edit through a redirect, if adding a section
+ */
+ public function testEdit_redirect() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEdit_redirect_$count";
+ $title = Title::newFromText( $name );
+ $page = WikiPage::factory( $title );
+
+ $rname = "Help:ApiEditPageTest_testEdit_redirect_r$count";
+ $rtitle = Title::newFromText( $rname );
+ $rpage = WikiPage::factory( $rtitle );
+
+ // base edit for content
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // base edit for redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
+ $this->forceRevisionDate( $rpage, '20120101000000' );
+
+ // conflicting edit to redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
+ $this->forceRevisionDate( $rpage, '20120101020202' );
+
+ // try to save edit, following the redirect
+ list( $re, , ) = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ 'section' => 'new',
+ 'redirect' => true,
+ ] );
+
+ $this->assertSame( 'Success', $re['edit']['result'],
+ "no problems expected when following redirect" );
+ }
+
+ /**
+ * Ensure we cannot edit through a redirect, if attempting to overwrite content
+ */
+ public function testEdit_redirectText() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEdit_redirectText_$count";
+ $title = Title::newFromText( $name );
+ $page = WikiPage::factory( $title );
+
+ $rname = "Help:ApiEditPageTest_testEdit_redirectText_r$count";
+ $rtitle = Title::newFromText( $rname );
+ $rpage = WikiPage::factory( $rtitle );
+
+ // base edit for content
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // base edit for redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
+ $this->forceRevisionDate( $rpage, '20120101000000' );
+
+ // conflicting edit to redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
+ $this->forceRevisionDate( $rpage, '20120101020202' );
+
+ // try to save edit, following the redirect but without creating a section
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ 'redirect' => true,
+ ] );
+
+ $this->fail( 'redirect-appendonly error expected' );
+ } catch ( ApiUsageException $ex ) {
+ $this->assertTrue( self::apiExceptionHasCode( $ex, 'redirect-appendonly' ) );
+ }
+ }
+
+ public function testEditConflict() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_$count";
+ $title = Title::newFromText( $name );
+
+ $page = WikiPage::factory( $title );
+
+ // base edit
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // conflicting edit
+ $page->doEditContent( new WikitextContent( "Foo bar" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
+ $this->forceRevisionDate( $page, '20120101020202' );
+
+ // try to save edit, expect conflict
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ ] );
+
+ $this->fail( 'edit conflict expected' );
+ } catch ( ApiUsageException $ex ) {
+ $this->assertTrue( self::apiExceptionHasCode( $ex, 'editconflict' ) );
+ }
+ }
+
+ /**
+ * Ensure that editing using section=new will prevent simple conflicts
+ */
+ public function testEditConflict_newSection() {
+ static $count = 0;
+ $count++;
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_newSection_$count";
+ $title = Title::newFromText( $name );
+
+ $page = WikiPage::factory( $title );
+
+ // base edit
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
+ $this->forceRevisionDate( $page, '20120101000000' );
+ $baseTime = $page->getRevision()->getTimestamp();
+
+ // conflicting edit
+ $page->doEditContent( new WikitextContent( "Foo bar" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
+ $this->forceRevisionDate( $page, '20120101020202' );
+
+ // try to save edit, expect no conflict
+ list( $re, , ) = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'nix bar!',
+ 'basetimestamp' => $baseTime,
+ 'section' => 'new',
+ ] );
+
+ $this->assertSame( 'Success', $re['edit']['result'],
+ "no edit conflict expected here" );
+ }
+
+ public function testEditConflict_bug41990() {
+ static $count = 0;
+ $count++;
+
+ /*
+ * T43990: if the target page has a newer revision than the redirect, then editing the
+ * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously
+ * caused an edit conflict to be detected.
+ */
+
+ // assume NS_HELP defaults to wikitext
+ $name = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_$count";
+ $title = Title::newFromText( $name );
+ $page = WikiPage::factory( $title );
+
+ $rname = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_r$count";
+ $rtitle = Title::newFromText( $rname );
+ $rpage = WikiPage::factory( $rtitle );
+
+ // base edit for content
+ $page->doEditContent( new WikitextContent( "Foo" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
+ $this->forceRevisionDate( $page, '20120101000000' );
+
+ // base edit for redirect
+ $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
+ "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
+ $this->forceRevisionDate( $rpage, '20120101000000' );
+
+ // new edit to content
+ $page->doEditContent( new WikitextContent( "Foo bar" ),
+ "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
+ $this->forceRevisionDate( $rpage, '20120101020202' );
+
+ // try to save edit; should work, following the redirect.
+ list( $re, , ) = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $rname,
+ 'text' => 'nix bar!',
+ 'section' => 'new',
+ 'redirect' => true,
+ ] );
+
+ $this->assertSame( 'Success', $re['edit']['result'],
+ "no edit conflict expected here" );
+ }
+
+ /**
+ * @param WikiPage $page
+ * @param string|int $timestamp
+ */
+ protected function forceRevisionDate( WikiPage $page, $timestamp ) {
+ $dbw = wfGetDB( DB_MASTER );
+
+ $dbw->update( 'revision',
+ [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
+ [ 'rev_id' => $page->getLatest() ] );
+
+ $page->clear();
+ }
+
+ public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
+ $this->setExpectedException(
+ ApiUsageException::class,
+ 'Direct editing via API is not supported for content model ' .
+ 'testing used by Dummy:ApiEditPageTest_nonTextPageEdit'
+ );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => 'Dummy:ApiEditPageTest_nonTextPageEdit',
+ 'text' => '{"animals":["kittens!"]}'
+ ] );
+ }
+
+ public function testSupportsDirectApiEditing_withContentHandlerOverride() {
+ $name = 'DummyNonText:ApiEditPageTest_testNonTextEdit';
+ $data = serialize( 'some bla bla text' );
+
+ $result = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => $data,
+ ] );
+
+ $apiResult = $result[0];
+
+ // Validate API result data
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertSame( 'Success', $apiResult['edit']['result'] );
+
+ $this->assertArrayHasKey( 'new', $apiResult['edit'] );
+ $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
+
+ $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
+
+ // validate resulting revision
+ $page = WikiPage::factory( Title::newFromText( $name ) );
+ $this->assertSame( "testing-nontext", $page->getContentModel() );
+ $this->assertSame( $data, $page->getContent()->serialize() );
+ }
+
+ /**
+ * This test verifies that after changing the content model
+ * of a page, undoing that edit via the API will also
+ * undo the content model change.
+ */
+ public function testUndoAfterContentModelChange() {
+ $name = 'Help:' . __FUNCTION__;
+ $uploader = self::$users['uploader']->getUser();
+ $sysop = self::$users['sysop']->getUser();
+
+ $apiResult = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ ], null, $sysop )[0];
+
+ // Check success
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertSame( 'Success', $apiResult['edit']['result'] );
+ $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
+ // Content model is wikitext
+ $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
+
+ // Convert the page to JSON
+ $apiResult = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => '{}',
+ 'contentmodel' => 'json',
+ ], null, $uploader )[0];
+
+ // Check success
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertSame( 'Success', $apiResult['edit']['result'] );
+ $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
+ $this->assertSame( 'json', $apiResult['edit']['contentmodel'] );
+
+ $apiResult = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'undo' => $apiResult['edit']['newrevid']
+ ], null, $sysop )[0];
+
+ // Check success
+ $this->assertArrayHasKey( 'edit', $apiResult );
+ $this->assertArrayHasKey( 'result', $apiResult['edit'] );
+ $this->assertSame( 'Success', $apiResult['edit']['result'] );
+ $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
+ // Check that the contentmodel is back to wikitext now.
+ $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
+ }
+
+ // The tests below are mostly not commented because they do exactly what
+ // you'd expect from the name.
+
+ public function testCorrectContentFormat() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ 'contentmodel' => 'wikitext',
+ 'contentformat' => 'text/x-wiki',
+ ] );
+
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ }
+
+ public function testUnsupportedContentFormat() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'Unrecognized value for parameter "contentformat": nonexistent format.' );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ 'contentformat' => 'nonexistent format',
+ ] );
+ } finally {
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ }
+ }
+
+ public function testMismatchedContentFormat() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The requested format text/plain is not supported for content ' .
+ "model wikitext used by $name." );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'some text',
+ 'contentmodel' => 'wikitext',
+ 'contentformat' => 'text/plain',
+ ] );
+ } finally {
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ }
+ }
+
+ public function testUndoToInvalidRev() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $revId = $this->editPage( $name, 'Some text' )->value['revision']
+ ->getId();
+ $revId++;
+
+ $this->setExpectedException( ApiUsageException::class,
+ "There is no revision with ID $revId." );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'undo' => $revId,
+ ] );
+ }
+
+ /**
+ * Tests what happens if the undo parameter is a valid revision, but
+ * the undoafter parameter doesn't refer to a revision that exists in the
+ * database.
+ */
+ public function testUndoAfterToInvalidRev() {
+ // We can't just pick a large number for undoafter (as in
+ // testUndoToInvalidRev above), because then MediaWiki will helpfully
+ // assume we switched around undo and undoafter and we'll test the code
+ // path for undo being invalid, not undoafter. So instead we delete
+ // the revision from the database. In real life this case could come
+ // up if a revision number was skipped, e.g., if two transactions try
+ // to insert new revision rows at once and the first one to succeed
+ // gets rolled back.
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+ $titleObj = Title::newFromText( $name );
+
+ $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
+ $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
+ $revId3 = $this->editPage( $name, '3' )->value['revision']->getId();
+
+ // Make the middle revision disappear
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->delete( 'revision', [ 'rev_id' => $revId2 ], __METHOD__ );
+ $dbw->update( 'revision', [ 'rev_parent_id' => $revId1 ],
+ [ 'rev_id' => $revId3 ], __METHOD__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ "There is no revision with ID $revId2." );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'undo' => $revId3,
+ 'undoafter' => $revId2,
+ ] );
+ }
+
+ /**
+ * Tests what happens if the undo parameter is a valid revision, but
+ * undoafter is hidden (rev_deleted).
+ */
+ public function testUndoAfterToHiddenRev() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+ $titleObj = Title::newFromText( $name );
+
+ $this->editPage( $name, '0' );
+
+ $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
+
+ $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
+
+ // Hide the middle revision
+ $list = RevisionDeleter::createList( 'revision',
+ RequestContext::getMain(), $titleObj, [ $revId1 ] );
+ $list->setVisibility( [
+ 'value' => [ Revision::DELETED_TEXT => 1 ],
+ 'comment' => 'Bye-bye',
+ ] );
+
+ $this->setExpectedException( ApiUsageException::class,
+ "There is no revision with ID $revId1." );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'undo' => $revId2,
+ 'undoafter' => $revId1,
+ ] );
+ }
+
+ /**
+ * Test undo when a revision with a higher id has an earlier timestamp.
+ * This can happen if importing an old revision.
+ */
+ public function testUndoWithSwappedRevisions() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+ $titleObj = Title::newFromText( $name );
+
+ $this->editPage( $name, '0' );
+
+ $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
+
+ $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
+
+ // Now monkey with the timestamp
+ $dbw = wfGetDB( DB_MASTER );
+ $dbw->update(
+ 'revision',
+ [ 'rev_timestamp' => wfTimestamp( TS_MW, time() - 86400 ) ],
+ [ 'rev_id' => $revId1 ],
+ __METHOD__
+ );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'undo' => $revId2,
+ 'undoafter' => $revId1,
+ ] );
+
+ $text = ( new WikiPage( $titleObj ) )->getContent()->getNativeData();
+
+ // This is wrong! It should be 1. But let's test for our incorrect
+ // behavior for now, so if someone fixes it they'll fix the test as
+ // well to expect 1. If we disabled the test, it might stay disabled
+ // even once the bug is fixed, which would be a shame.
+ $this->assertSame( '2', $text );
+ }
+
+ public function testUndoWithConflicts() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The edit could not be undone due to conflicting intermediate edits.' );
+
+ $this->editPage( $name, '1' );
+
+ $revId = $this->editPage( $name, '2' )->value['revision']->getId();
+
+ $this->editPage( $name, '3' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'undo' => $revId,
+ ] );
+
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
+ ->getNativeData();
+ $this->assertSame( '3', $text );
+ }
+
+ /**
+ * undoafter is supposed to be less than undo. If not, we reverse their
+ * meaning, so that the two are effectively interchangeable.
+ */
+ public function testReversedUndoAfter() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, '0' );
+ $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
+ $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'undo' => $revId1,
+ 'undoafter' => $revId2,
+ ] );
+
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
+ ->getNativeData();
+ $this->assertSame( '1', $text );
+ }
+
+ public function testUndoToRevFromDifferentPage() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( "$name-1", 'Some text' );
+ $revId = $this->editPage( "$name-1", 'Some more text' )
+ ->value['revision']->getId();
+
+ $this->editPage( "$name-2", 'Some text' );
+
+ $this->setExpectedException( ApiUsageException::class,
+ "r$revId is not a revision of $name-2." );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => "$name-2",
+ 'undo' => $revId,
+ ] );
+ }
+
+ public function testUndoAfterToRevFromDifferentPage() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $revId1 = $this->editPage( "$name-1", 'Some text' )
+ ->value['revision']->getId();
+
+ $revId2 = $this->editPage( "$name-2", 'Some text' )
+ ->value['revision']->getId();
+
+ $this->setExpectedException( ApiUsageException::class,
+ "r$revId1 is not a revision of $name-2." );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => "$name-2",
+ 'undo' => $revId2,
+ 'undoafter' => $revId1,
+ ] );
+ }
+
+ public function testMd5Text() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ 'md5' => md5( 'Some text' ),
+ ] );
+
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ }
+
+ public function testMd5PrependText() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Some text' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'prependtext' => 'Alert: ',
+ 'md5' => md5( 'Alert: ' ),
+ ] );
+
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+ $this->assertSame( 'Alert: Some text', $text );
+ }
+
+ public function testMd5AppendText() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Some text' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => ' is nice',
+ 'md5' => md5( ' is nice' ),
+ ] );
+
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+ $this->assertSame( 'Some text is nice', $text );
+ }
+
+ public function testMd5PrependAndAppendText() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Some text' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'prependtext' => 'Alert: ',
+ 'appendtext' => ' is nice',
+ 'md5' => md5( 'Alert: is nice' ),
+ ] );
+
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+ $this->assertSame( 'Alert: Some text is nice', $text );
+ }
+
+ public function testIncorrectMd5Text() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The supplied MD5 hash was incorrect.' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ 'md5' => md5( '' ),
+ ] );
+ }
+
+ public function testIncorrectMd5PrependText() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The supplied MD5 hash was incorrect.' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'prependtext' => 'Some ',
+ 'appendtext' => 'text',
+ 'md5' => md5( 'Some ' ),
+ ] );
+ }
+
+ public function testIncorrectMd5AppendText() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The supplied MD5 hash was incorrect.' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'prependtext' => 'Some ',
+ 'appendtext' => 'text',
+ 'md5' => md5( 'text' ),
+ ] );
+ }
+
+ public function testCreateOnly() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The article you tried to create has been created already.' );
+
+ $this->editPage( $name, 'Some text' );
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some more text',
+ 'createonly' => '',
+ ] );
+ } finally {
+ // Validate that content was not changed
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+
+ $this->assertSame( 'Some text', $text );
+ }
+ }
+
+ public function testNoCreate() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ "The page you specified doesn't exist." );
+
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ 'nocreate' => '',
+ ] );
+ } finally {
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ }
+ }
+
+ /**
+ * Appending/prepending is currently only supported for TextContent. We
+ * test this right now, and when support is added this test should be
+ * replaced by tests that the support is correct.
+ */
+ public function testAppendWithNonTextContentHandler() {
+ $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ "Can't append to pages using content model testing-nontext." );
+
+ $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
+ function ( Title $title, &$model ) use ( $name ) {
+ if ( $title->getPrefixedText() === $name ) {
+ $model = 'testing-nontext';
+ }
+ return true;
+ }
+ );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => 'Some text',
+ ] );
+ }
+
+ public function testAppendInMediaWikiNamespace() {
+ $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
+
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => 'Some text',
+ ] );
+
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ }
+
+ public function testAppendInMediaWikiNamespaceWithSerializationError() {
+ $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'Content serialization failed: Could not unserialize content' );
+
+ $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
+ function ( Title $title, &$model ) use ( $name ) {
+ if ( $title->getPrefixedText() === $name ) {
+ $model = 'testing-serialize-error';
+ }
+ return true;
+ }
+ );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => 'Some text',
+ ] );
+ }
+
+ public function testAppendNewSection() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Initial content' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => '== New section ==',
+ 'section' => 'new',
+ ] );
+
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+
+ $this->assertSame( "Initial content\n\n== New section ==", $text );
+ }
+
+ public function testAppendNewSectionWithInvalidContentModel() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'Sections are not supported for content model text.' );
+
+ $this->editPage( $name, 'Initial content' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => '== New section ==',
+ 'section' => 'new',
+ 'contentmodel' => 'text',
+ ] );
+ }
+
+ public function testAppendNewSectionWithTitle() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Initial content' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'sectiontitle' => 'My section',
+ 'appendtext' => 'More content',
+ 'section' => 'new',
+ ] );
+
+ $page = new WikiPage( Title::newFromText( $name ) );
+
+ $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
+ $page->getContent()->getNativeData() );
+ $this->assertSame( '/* My section */ new section',
+ $page->getRevision()->getComment() );
+ }
+
+ public function testAppendNewSectionWithSummary() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Initial content' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => 'More content',
+ 'section' => 'new',
+ 'summary' => 'Add new section',
+ ] );
+
+ $page = new WikiPage( Title::newFromText( $name ) );
+
+ $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content",
+ $page->getContent()->getNativeData() );
+ // EditPage actually assumes the summary is the section name here
+ $this->assertSame( '/* Add new section */ new section',
+ $page->getRevision()->getComment() );
+ }
+
+ public function testAppendNewSectionWithTitleAndSummary() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Initial content' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'sectiontitle' => 'My section',
+ 'appendtext' => 'More content',
+ 'section' => 'new',
+ 'summary' => 'Add new section',
+ ] );
+
+ $page = new WikiPage( Title::newFromText( $name ) );
+
+ $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
+ $page->getContent()->getNativeData() );
+ $this->assertSame( 'Add new section',
+ $page->getRevision()->getComment() );
+ }
+
+ public function testAppendToSection() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, "== Section 1 ==\n\nContent\n\n" .
+ "== Section 2 ==\n\nFascinating!" );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => ' and more content',
+ 'section' => '1',
+ ] );
+
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+
+ $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" .
+ "== Section 2 ==\n\nFascinating!", $text );
+ }
+
+ public function testAppendToFirstSection() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, "Content\n\n== Section 1 ==\n\nFascinating!" );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => ' and more content',
+ 'section' => '0',
+ ] );
+
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+
+ $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" .
+ "Fascinating!", $text );
+ }
+
+ public function testAppendToNonexistentSection() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class, 'There is no section 1.' );
+
+ $this->editPage( $name, 'Content' );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'appendtext' => ' and more content',
+ 'section' => '1',
+ ] );
+ } finally {
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+
+ $this->assertSame( 'Content', $text );
+ }
+ }
+
+ public function testEditMalformedSection() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The "section" parameter must be a valid section ID or "new".' );
+ $this->editPage( $name, 'Content' );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Different content',
+ 'section' => 'It is unlikely that this is valid',
+ ] );
+ } finally {
+ $text = ( new WikiPage( Title::newFromText( $name ) ) )
+ ->getContent()->getNativeData();
+
+ $this->assertSame( 'Content', $text );
+ }
+ }
+
+ public function testEditWithStartTimestamp() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+ $this->setExpectedException( ApiUsageException::class,
+ 'The page has been deleted since you fetched its timestamp.' );
+
+ $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
+
+ $this->editPage( $name, 'Some text' );
+
+ $pageObj = new WikiPage( Title::newFromText( $name ) );
+ $pageObj->doDeleteArticle( 'Bye-bye' );
+
+ $this->assertFalse( $pageObj->exists() );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Different text',
+ 'starttimestamp' => $startTime,
+ ] );
+ } finally {
+ $this->assertFalse( $pageObj->exists() );
+ }
+ }
+
+ public function testEditMinor() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->editPage( $name, 'Some text' );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Different text',
+ 'minor' => '',
+ ] );
+
+ $revisionStore = \MediaWiki\MediaWikiServices::getInstance()->getRevisionStore();
+ $revision = $revisionStore->getRevisionByTitle( Title::newFromText( $name ) );
+ $this->assertTrue( $revision->isMinor() );
+ }
+
+ public function testEditRecreate() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
+
+ $this->editPage( $name, 'Some text' );
+
+ $pageObj = new WikiPage( Title::newFromText( $name ) );
+ $pageObj->doDeleteArticle( 'Bye-bye' );
+
+ $this->assertFalse( $pageObj->exists() );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Different text',
+ 'starttimestamp' => $startTime,
+ 'recreate' => '',
+ ] );
+
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ }
+
+ public function testEditWatch() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+ $user = self::$users['sysop']->getUser();
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ 'watch' => '',
+ ] );
+
+ $this->assertTrue( Title::newFromText( $name )->exists() );
+ $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
+ }
+
+ public function testEditUnwatch() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+ $user = self::$users['sysop']->getUser();
+ $titleObj = Title::newFromText( $name );
+
+ $user->addWatch( $titleObj );
+
+ $this->assertFalse( $titleObj->exists() );
+ $this->assertTrue( $user->isWatched( $titleObj ) );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ 'unwatch' => '',
+ ] );
+
+ $this->assertTrue( $titleObj->exists() );
+ $this->assertFalse( $user->isWatched( $titleObj ) );
+ }
+
+ public function testEditWithTag() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ ChangeTags::defineTag( 'custom tag' );
+
+ $revId = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ 'tags' => 'custom tag',
+ ] )[0]['edit']['newrevid'];
+
+ $dbw = wfGetDB( DB_MASTER );
+ $this->assertSame( 'custom tag', $dbw->selectField(
+ 'change_tag', 'ct_tag', [ 'ct_rev_id' => $revId ], __METHOD__ ) );
+ }
+
+ public function testEditWithoutTagPermission() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'You do not have permission to apply change tags along with your changes.' );
+
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+
+ ChangeTags::defineTag( 'custom tag' );
+ $this->setMwGlobals( 'wgRevokePermissions',
+ [ 'user' => [ 'applychangetags' => true ] ] );
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ 'tags' => 'custom tag',
+ ] );
+ } finally {
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ }
+ }
+
+ public function testEditAbortedByHook() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The modification you tried to make was aborted by an extension.' );
+
+ $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
+ 'hook-APIEditBeforeSave-closure)' );
+
+ $this->setTemporaryHook( 'APIEditBeforeSave',
+ function () {
+ return false;
+ }
+ );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ ] );
+ } finally {
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ }
+ }
+
+ public function testEditAbortedByHookWithCustomOutput() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
+ 'hook-APIEditBeforeSave-closure)' );
+
+ $this->setTemporaryHook( 'APIEditBeforeSave',
+ function ( $unused1, $unused2, &$r ) {
+ $r['msg'] = 'Some message';
+ return false;
+ } );
+
+ $result = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ ] );
+ Wikimedia\restoreWarnings();
+
+ $this->assertSame( [ 'msg' => 'Some message', 'result' => 'Failure' ],
+ $result[0]['edit'] );
+
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ }
+
+ public function testEditAbortedByEditPageHookWithResult() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setTemporaryHook( 'EditFilterMergedContent',
+ function ( $unused1, $unused2, Status $status ) {
+ $status->apiHookResult = [ 'msg' => 'A message for you!' ];
+ return false;
+ } );
+
+ $res = $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ ] );
+
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ $this->assertSame( [ 'edit' => [ 'msg' => 'A message for you!',
+ 'result' => 'Failure' ] ], $res[0] );
+ }
+
+ public function testEditAbortedByEditPageHookWithNoResult() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The modification you tried to make was aborted by an extension.' );
+
+ $this->setTemporaryHook( 'EditFilterMergedContent',
+ function () {
+ return false;
+ }
+ );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ ] );
+ } finally {
+ $this->assertFalse( Title::newFromText( $name )->exists() );
+ }
+ }
+
+ public function testEditWhileBlocked() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'You have been blocked from editing.' );
+
+ $block = new Block( [
+ 'address' => self::$users['sysop']->getUser()->getName(),
+ 'by' => self::$users['sysop']->getUser()->getId(),
+ 'reason' => 'Capriciousness',
+ 'timestamp' => '19370101000000',
+ 'expiry' => 'infinity',
+ ] );
+ $block->insert();
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ ] );
+ } finally {
+ $block->delete();
+ self::$users['sysop']->getUser()->clearInstanceCache();
+ }
+ }
+
+ public function testEditWhileReadOnly() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The wiki is currently in read-only mode.' );
+
+ $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
+ $svc->setReason( "Read-only for testing" );
+
+ try {
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ ] );
+ } finally {
+ $svc->setReason( false );
+ }
+ }
+
+ public function testCreateImageRedirectAnon() {
+ $name = 'File:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ "Anonymous users can't create image redirects." );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => '#REDIRECT [[File:Other file.png]]',
+ ], null, new User() );
+ }
+
+ public function testCreateImageRedirectLoggedIn() {
+ $name = 'File:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ "You don't have permission to create image redirects." );
+
+ $this->setMwGlobals( 'wgRevokePermissions',
+ [ 'user' => [ 'upload' => true ] ] );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => '#REDIRECT [[File:Other file.png]]',
+ ] );
+ }
+
+ public function testTooBigEdit() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The content you supplied exceeds the article size limit of 1 kilobyte.' );
+
+ $this->setMwGlobals( 'wgMaxArticleSize', 1 );
+
+ $text = str_repeat( '!', 1025 );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => $text,
+ ] );
+ }
+
+ public function testProhibitedAnonymousEdit() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ 'The action you have requested is limited to users in the group: ' );
+
+ $this->setMwGlobals( 'wgRevokePermissions', [ '*' => [ 'edit' => true ] ] );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ ], null, new User() );
+ }
+
+ public function testProhibitedChangeContentModel() {
+ $name = 'Help:' . ucfirst( __FUNCTION__ );
+
+ $this->setExpectedException( ApiUsageException::class,
+ "You don't have permission to change the content model of a page." );
+
+ $this->setMwGlobals( 'wgRevokePermissions',
+ [ 'user' => [ 'editcontentmodel' => true ] ] );
+
+ $this->doApiRequestWithToken( [
+ 'action' => 'edit',
+ 'title' => $name,
+ 'text' => 'Some text',
+ 'contentmodel' => 'json',
+ ] );
+ }
+}