This adds supportsDirectApiEditing and supportsDirectEditing methods to ContentHandler. Both return false by default for the ContentHandler base class, and true for TextContentHandler and it's derivatives. (everything in core) Extension content types that directly extend AbstractContent / ContentHandler, often / generally don't support direct editing. EntityContent in Wikibase and Flow boards are the two such content types currently in gerrit-hosted extensions. The use and direct settings of the allowNonTextContent member variable is replaced by enableApiEditOverride and a setter for that. The only place allowNonTextContent is used in all of Wikimedia-hosted git repos is core itself (EditPage and ApiEditPage), so should be safe to make this change. With this change, Wikibase can remove its ApiCheckCanExecute hook handler that disallows editing there, and MobileFrontend could check if direct editing is allowed before enabling it's editing features, instead of Wikibase having to add MobileFrontend hook handlers to disable the features. Bug: T96382 Change-Id: I276cd6ecedf38108f1f2be16b38e699e8c5d2d0c
515 lines
16 KiB
PHP
515 lines
16 KiB
PHP
<?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( array(
|
|
'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';
|
|
|
|
MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
|
|
$wgContLang->resetNamespaces(); # reset namespace cache
|
|
|
|
$this->doLogin();
|
|
}
|
|
|
|
protected function tearDown() {
|
|
MWNamespace::getCanonicalNamespaces( true ); # 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( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'some text',
|
|
) );
|
|
$apiResult = $apiResult[0];
|
|
|
|
// Validate API result data
|
|
$this->assertArrayHasKey( 'edit', $apiResult );
|
|
$this->assertArrayHasKey( 'result', $apiResult['edit'] );
|
|
$this->assertEquals( '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( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'some text',
|
|
) );
|
|
|
|
$this->assertEquals( '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( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'different text'
|
|
) );
|
|
|
|
$this->assertEquals( '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 array(
|
|
array( #0: append
|
|
'foo', 'append', 'bar', "foobar"
|
|
),
|
|
array( #1: prepend
|
|
'foo', 'prepend', 'bar', "barfoo"
|
|
),
|
|
array( #2: append to empty page
|
|
'', 'append', 'foo', "foo"
|
|
),
|
|
array( #3: prepend to empty page
|
|
'', 'prepend', 'foo', "foo"
|
|
),
|
|
array( #4: append to non-existing page
|
|
null, 'append', 'foo', "foo"
|
|
),
|
|
array( #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( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => $text, ) );
|
|
|
|
$this->assertEquals( 'Success', $re['edit']['result'] ); // sanity
|
|
}
|
|
|
|
// -- try append/prepend --------------------------------------------
|
|
list( $re ) = $this->doApiRequestWithToken( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
$op . 'text' => $append, ) );
|
|
|
|
$this->assertEquals( '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->assertEquals( $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( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'section' => '1',
|
|
'text' => "==section 1==\nnew content 1",
|
|
) );
|
|
$this->assertEquals( 'Success', $re['edit']['result'] );
|
|
$newtext = WikiPage::factory( Title::newFromText( $name ) )
|
|
->getContent( Revision::RAW )
|
|
->getNativeData();
|
|
$this->assertEquals( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext );
|
|
|
|
// Test that we raise a 'nosuchsection' error
|
|
try {
|
|
$this->doApiRequestWithToken( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'section' => '9999',
|
|
'text' => 'text',
|
|
) );
|
|
$this->fail( "Should have raised a UsageException" );
|
|
} catch ( UsageException $e ) {
|
|
$this->assertEquals( 'nosuchsection', $e->getCodeString() );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Test action=edit§ion=new
|
|
* Run it twice so we test adding a new section on a
|
|
* page that doesn't exist (bug 52830) 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( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'section' => 'new',
|
|
'text' => 'test',
|
|
'summary' => 'header',
|
|
));
|
|
|
|
$this->assertEquals( 'Success', $re['edit']['result'] );
|
|
// Check the page text is correct
|
|
$text = WikiPage::factory( Title::newFromText( $name ) )
|
|
->getContent( Revision::RAW )
|
|
->getNativeData();
|
|
$this->assertEquals( "== header ==\n\ntest", $text );
|
|
|
|
// Now on one that does
|
|
$this->assertTrue( Title::newFromText( $name )->exists() );
|
|
list( $re2 ) = $this->doApiRequestWithToken( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'section' => 'new',
|
|
'text' => 'test',
|
|
'summary' => 'header',
|
|
));
|
|
|
|
$this->assertEquals( 'Success', $re2['edit']['result'] );
|
|
$text = WikiPage::factory( Title::newFromText( $name ) )
|
|
->getContent( Revision::RAW )
|
|
->getNativeData();
|
|
$this->assertEquals( "== 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']->user );
|
|
$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']->user );
|
|
$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']->user );
|
|
$this->forceRevisionDate( $rpage, '20120101020202' );
|
|
|
|
// try to save edit, following the redirect
|
|
list( $re, , ) = $this->doApiRequestWithToken( array(
|
|
'action' => 'edit',
|
|
'title' => $rname,
|
|
'text' => 'nix bar!',
|
|
'basetimestamp' => $baseTime,
|
|
'section' => 'new',
|
|
'redirect' => true,
|
|
), null, self::$users['sysop']->user );
|
|
|
|
$this->assertEquals( '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']->user );
|
|
$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']->user );
|
|
$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']->user );
|
|
$this->forceRevisionDate( $rpage, '20120101020202' );
|
|
|
|
// try to save edit, following the redirect but without creating a section
|
|
try {
|
|
$this->doApiRequestWithToken( array(
|
|
'action' => 'edit',
|
|
'title' => $rname,
|
|
'text' => 'nix bar!',
|
|
'basetimestamp' => $baseTime,
|
|
'redirect' => true,
|
|
), null, self::$users['sysop']->user );
|
|
|
|
$this->fail( 'redirect-appendonly error expected' );
|
|
} catch ( UsageException $ex ) {
|
|
$this->assertEquals( 'redirect-appendonly', $ex->getCodeString() );
|
|
}
|
|
}
|
|
|
|
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']->user );
|
|
$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']->user );
|
|
$this->forceRevisionDate( $page, '20120101020202' );
|
|
|
|
// try to save edit, expect conflict
|
|
try {
|
|
$this->doApiRequestWithToken( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'nix bar!',
|
|
'basetimestamp' => $baseTime,
|
|
), null, self::$users['sysop']->user );
|
|
|
|
$this->fail( 'edit conflict expected' );
|
|
} catch ( UsageException $ex ) {
|
|
$this->assertEquals( 'editconflict', $ex->getCodeString() );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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']->user );
|
|
$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']->user );
|
|
$this->forceRevisionDate( $page, '20120101020202' );
|
|
|
|
// try to save edit, expect no conflict
|
|
list( $re, , ) = $this->doApiRequestWithToken( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'nix bar!',
|
|
'basetimestamp' => $baseTime,
|
|
'section' => 'new',
|
|
), null, self::$users['sysop']->user );
|
|
|
|
$this->assertEquals( 'Success', $re['edit']['result'],
|
|
"no edit conflict expected here" );
|
|
}
|
|
|
|
public function testEditConflict_bug41990() {
|
|
static $count = 0;
|
|
$count++;
|
|
|
|
/*
|
|
* bug 41990: 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']->user );
|
|
$this->forceRevisionDate( $page, '20120101000000' );
|
|
|
|
// base edit for redirect
|
|
$rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
|
|
"testing 1", EDIT_NEW, false, self::$users['sysop']->user );
|
|
$this->forceRevisionDate( $rpage, '20120101000000' );
|
|
|
|
// new edit to content
|
|
$page->doEditContent( new WikitextContent( "Foo bar" ),
|
|
"testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->user );
|
|
$this->forceRevisionDate( $rpage, '20120101020202' );
|
|
|
|
// try to save edit; should work, following the redirect.
|
|
list( $re, , ) = $this->doApiRequestWithToken( array(
|
|
'action' => 'edit',
|
|
'title' => $rname,
|
|
'text' => 'nix bar!',
|
|
'section' => 'new',
|
|
'redirect' => true,
|
|
), null, self::$users['sysop']->user );
|
|
|
|
$this->assertEquals( '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',
|
|
array( 'rev_timestamp' => $dbw->timestamp( $timestamp ) ),
|
|
array( 'rev_id' => $page->getLatest() ) );
|
|
|
|
$page->clear();
|
|
}
|
|
|
|
public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
|
|
$this->setExpectedException(
|
|
'UsageException',
|
|
'Direct editing via API is not supported for this content type.'
|
|
);
|
|
|
|
$this->doApiRequestWithToken( array(
|
|
'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( array(
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => $data,
|
|
) );
|
|
|
|
$apiResult = $result[0];
|
|
|
|
// Validate API result data
|
|
$this->assertArrayHasKey( 'edit', $apiResult );
|
|
$this->assertArrayHasKey( 'result', $apiResult['edit'] );
|
|
$this->assertEquals( '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->assertEquals( "testing-nontext", $page->getContentModel() );
|
|
$this->assertEquals( $data, $page->getContent()->serialize() );
|
|
}
|
|
}
|