composer: * mediawiki/mediawiki-codesniffer: 36.0.0 → 37.0.0 The following sniffs now pass and were enabled: * Generic.ControlStructures.InlineControlStructure * MediaWiki.PHPUnit.AssertCount.NotUsed npm: * svgo: 2.3.0 → 2.3.1 * https://npmjs.com/advisories/1754 (CVE-2021-33587) Change-Id: I2a9bbee2fecbf7259876d335f565ece4b3622426
1742 lines
48 KiB
PHP
1742 lines
48 KiB
PHP
<?php
|
|
|
|
use MediaWiki\Block\DatabaseBlock;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Revision\RevisionRecord;
|
|
|
|
/**
|
|
* 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(): void {
|
|
parent::setUp();
|
|
|
|
$this->setMwGlobals( [
|
|
'wgExtraNamespaces' => [
|
|
12312 => 'Dummy',
|
|
12313 => 'Dummy_talk',
|
|
12314 => 'DummyNonText',
|
|
12315 => 'DummyNonText_talk',
|
|
],
|
|
'wgNamespaceContentModels' => [
|
|
12312 => 'testing',
|
|
12314 => 'testing-nontext',
|
|
],
|
|
'wgWatchlistExpiry' => true,
|
|
'wgWatchlistExpiryMaxDuration' => '6 months',
|
|
] );
|
|
$this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
|
|
'testing' => 'DummyContentHandlerForTesting',
|
|
'testing-nontext' => 'DummyNonTextContentHandler',
|
|
'testing-serialize-error' => 'DummySerializeErrorContentHandler',
|
|
] );
|
|
$this->tablesUsed = array_merge(
|
|
$this->tablesUsed,
|
|
[ 'change_tag', 'change_tag_def', 'logging', 'watchlist', 'watchlist_expiry' ]
|
|
);
|
|
}
|
|
|
|
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->getText();
|
|
|
|
$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->doUserEditContent(
|
|
ContentHandler::makeContent( $text, $page->getTitle() ),
|
|
$this->getTestSysop()->getUser(),
|
|
'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( RevisionRecord::RAW )
|
|
->getText();
|
|
$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§ion=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( RevisionRecord::RAW )
|
|
->getText();
|
|
$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( RevisionRecord::RAW )
|
|
->getText();
|
|
$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->doUserEditContent(
|
|
new WikitextContent( "Foo" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101000000' );
|
|
$baseTime = $page->getRevisionRecord()->getTimestamp();
|
|
|
|
// base edit for redirect
|
|
$rpage->doUserEditContent(
|
|
new WikitextContent( "#REDIRECT [[$name]]" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $rpage, '20120101000000' );
|
|
|
|
// conflicting edit to redirect
|
|
$rpage->doUserEditContent(
|
|
new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
|
|
self::$users['uploader']->getUser(),
|
|
"testing 2",
|
|
EDIT_UPDATE,
|
|
$page->getLatest()
|
|
);
|
|
$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->doUserEditContent(
|
|
new WikitextContent( "Foo" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101000000' );
|
|
$baseTime = $page->getRevisionRecord()->getTimestamp();
|
|
|
|
// base edit for redirect
|
|
$rpage->doUserEditContent(
|
|
new WikitextContent( "#REDIRECT [[$name]]" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $rpage, '20120101000000' );
|
|
|
|
// conflicting edit to redirect
|
|
$rpage->doUserEditContent(
|
|
new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
|
|
self::$users['uploader']->getUser(),
|
|
"testing 2",
|
|
EDIT_UPDATE,
|
|
$page->getLatest()
|
|
);
|
|
$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_revid() {
|
|
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->doUserEditContent(
|
|
new WikitextContent( "Foo" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101000000' );
|
|
$baseId = $page->getRevisionRecord()->getId();
|
|
|
|
// conflicting edit
|
|
$page->doUserEditContent(
|
|
new WikitextContent( "Foo bar" ),
|
|
self::$users['uploader']->getUser(),
|
|
"testing 2",
|
|
EDIT_UPDATE,
|
|
$page->getLatest()
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101020202' );
|
|
|
|
// try to save edit, expect conflict
|
|
try {
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'nix bar!',
|
|
'baserevid' => $baseId,
|
|
], null, self::$users['sysop']->getUser() );
|
|
|
|
$this->fail( 'edit conflict expected' );
|
|
} catch ( ApiUsageException $ex ) {
|
|
$this->assertTrue( self::apiExceptionHasCode( $ex, 'editconflict' ) );
|
|
}
|
|
}
|
|
|
|
public function testEditConflict_timestamp() {
|
|
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->doUserEditContent(
|
|
new WikitextContent( "Foo" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101000000' );
|
|
$baseTime = $page->getRevisionRecord()->getTimestamp();
|
|
|
|
// conflicting edit
|
|
$page->doUserEditContent(
|
|
new WikitextContent( "Foo bar" ),
|
|
self::$users['uploader']->getUser(),
|
|
"testing 2",
|
|
EDIT_UPDATE,
|
|
$page->getLatest()
|
|
);
|
|
$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->doUserEditContent(
|
|
new WikitextContent( "Foo" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101000000' );
|
|
$baseTime = $page->getRevisionRecord()->getTimestamp();
|
|
|
|
// conflicting edit
|
|
$page->doUserEditContent(
|
|
new WikitextContent( "Foo bar" ),
|
|
self::$users['uploader']->getUser(),
|
|
"testing 2",
|
|
EDIT_UPDATE,
|
|
$page->getLatest()
|
|
);
|
|
$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_T43990() {
|
|
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_T43990_$count";
|
|
$title = Title::newFromText( $name );
|
|
$page = WikiPage::factory( $title );
|
|
|
|
$rname = "Help:ApiEditPageTest_testEditConflict_redirect_T43990_r$count";
|
|
$rtitle = Title::newFromText( $rname );
|
|
$rpage = WikiPage::factory( $rtitle );
|
|
|
|
// base edit for content
|
|
$page->doUserEditContent(
|
|
new WikitextContent( "Foo" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101000000' );
|
|
|
|
// base edit for redirect
|
|
$rpage->doUserEditContent(
|
|
new WikitextContent( "#REDIRECT [[$name]]" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $rpage, '20120101000000' );
|
|
|
|
// new edit to content
|
|
$page->doUserEditContent(
|
|
new WikitextContent( "Foo bar" ),
|
|
self::$users['uploader']->getUser(),
|
|
"testing 2",
|
|
EDIT_UPDATE,
|
|
$page->getLatest()
|
|
);
|
|
$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_PRIMARY );
|
|
|
|
$dbw->update( 'revision',
|
|
[ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
|
|
[ 'rev_id' => $page->getLatest() ] );
|
|
|
|
$page->clear();
|
|
}
|
|
|
|
public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
'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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
'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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
'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-record']
|
|
->getId();
|
|
$revId++;
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "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-record']->getId();
|
|
$revId2 = $this->editPage( $name, '2' )->value['revision-record']->getId();
|
|
$revId3 = $this->editPage( $name, '3' )->value['revision-record']->getId();
|
|
|
|
// Make the middle revision disappear
|
|
$dbw = wfGetDB( DB_PRIMARY );
|
|
$dbw->delete( 'revision', [ 'rev_id' => $revId2 ], __METHOD__ );
|
|
$dbw->update( 'revision', [ 'rev_parent_id' => $revId1 ],
|
|
[ 'rev_id' => $revId3 ], __METHOD__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "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-record']->getId();
|
|
|
|
$revId2 = $this->editPage( $name, '2' )->value['revision-record']->getId();
|
|
|
|
// Hide the middle revision
|
|
$list = RevisionDeleter::createList( 'revision',
|
|
RequestContext::getMain(), $titleObj, [ $revId1 ] );
|
|
$list->setVisibility( [
|
|
'value' => [ RevisionRecord::DELETED_TEXT => 1 ],
|
|
'comment' => 'Bye-bye',
|
|
] );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "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() {
|
|
$this->markTestSkippedIfNoDiff3();
|
|
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
$titleObj = Title::newFromText( $name );
|
|
|
|
$this->editPage( $name, '0' );
|
|
|
|
$revId2 = $this->editPage( $name, '2' )->value['revision-record']->getId();
|
|
|
|
$revId1 = $this->editPage( $name, '1' )->value['revision-record']->getId();
|
|
|
|
// Now monkey with the timestamp
|
|
$dbw = wfGetDB( DB_PRIMARY );
|
|
$dbw->update(
|
|
'revision',
|
|
[ 'rev_timestamp' => $dbw->timestamp( time() - 86400 ) ],
|
|
[ 'rev_id' => $revId1 ],
|
|
__METHOD__
|
|
);
|
|
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'undo' => $revId2,
|
|
'undoafter' => $revId1,
|
|
] );
|
|
|
|
$text = ( new WikiPage( $titleObj ) )->getContent()->getText();
|
|
|
|
$this->assertSame( '1', $text );
|
|
}
|
|
|
|
public function testUndoWithConflicts() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
'The edit could not be undone due to conflicting intermediate edits.'
|
|
);
|
|
|
|
$this->editPage( $name, '1' );
|
|
|
|
$revId = $this->editPage( $name, '2' )->value['revision-record']->getId();
|
|
|
|
$this->editPage( $name, '3' );
|
|
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'undo' => $revId,
|
|
] );
|
|
|
|
$text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
|
|
->getText();
|
|
$this->assertSame( '3', $text );
|
|
}
|
|
|
|
public function testReversedUndoAfter() {
|
|
$this->markTestSkippedIfNoDiff3();
|
|
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( $name, '0' );
|
|
$revId1 = $this->editPage( $name, '1' )->value['revision-record']->getId();
|
|
$revId2 = $this->editPage( $name, '2' )->value['revision-record']->getId();
|
|
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'undo' => $revId1,
|
|
'undoafter' => $revId2,
|
|
] );
|
|
|
|
$text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
|
|
->getText();
|
|
$this->assertSame( '2', $text );
|
|
}
|
|
|
|
public function testUndoToRevFromDifferentPage() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->editPage( "$name-1", 'Some text' );
|
|
$revId = $this->editPage( "$name-1", 'Some more text' )
|
|
->value['revision-record']->getId();
|
|
|
|
$this->editPage( "$name-2", 'Some text' );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "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-record']->getId();
|
|
|
|
$revId2 = $this->editPage( "$name-2", 'Some text' )
|
|
->value['revision-record']->getId();
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "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()->getText();
|
|
$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()->getText();
|
|
$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()->getText();
|
|
$this->assertSame( 'Alert: Some text is nice', $text );
|
|
}
|
|
|
|
public function testIncorrectMd5Text() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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()->getText();
|
|
|
|
$this->assertSame( 'Some text', $text );
|
|
}
|
|
}
|
|
|
|
public function testNoCreate() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "Can't append to pages using content model testing-nontext." );
|
|
|
|
$this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
|
|
static 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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( 'Content serialization failed: Could not unserialize content' );
|
|
|
|
$this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
|
|
static 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()->getText();
|
|
|
|
$this->assertSame( "Initial content\n\n== New section ==", $text );
|
|
}
|
|
|
|
public function testAppendNewSectionWithInvalidContentModel() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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()->getText() );
|
|
$comment = $page->getRevisionRecord()->getComment();
|
|
$this->assertInstanceOf( CommentStoreComment::class, $comment );
|
|
$this->assertSame( '/* My section */ new section', $comment->text );
|
|
}
|
|
|
|
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()->getText() );
|
|
// EditPage actually assumes the summary is the section name here
|
|
$comment = $page->getRevisionRecord()->getComment();
|
|
$this->assertInstanceOf( CommentStoreComment::class, $comment );
|
|
$this->assertSame( '/* Add new section */ new section', $comment->text );
|
|
}
|
|
|
|
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()->getText() );
|
|
$comment = $page->getRevisionRecord()->getComment();
|
|
$this->assertInstanceOf( CommentStoreComment::class, $comment );
|
|
$this->assertSame( 'Add new section', $comment->text );
|
|
}
|
|
|
|
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()->getText();
|
|
|
|
$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()->getText();
|
|
|
|
$this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" .
|
|
"Fascinating!", $text );
|
|
}
|
|
|
|
public function testAppendToNonexistentSection() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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()->getText();
|
|
|
|
$this->assertSame( 'Content', $text );
|
|
}
|
|
}
|
|
|
|
public function testEditMalformedSection() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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()->getText();
|
|
|
|
$this->assertSame( 'Content', $text );
|
|
}
|
|
}
|
|
|
|
public function testEditWithStartTimestamp() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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->doDeleteArticleReal( 'Bye-bye', $this->getTestSysop()->getUser() );
|
|
|
|
$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->doDeleteArticleReal( 'Bye-bye', $this->getTestSysop()->getUser() );
|
|
|
|
$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();
|
|
$watchlistManager = $this->getServiceContainer()->getWatchlistManager();
|
|
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'Some text',
|
|
'watch' => '',
|
|
'watchlistexpiry' => '99990123000000',
|
|
] );
|
|
|
|
$title = Title::newFromText( $name );
|
|
$this->assertTrue( $title->exists() );
|
|
$this->assertTrue( $watchlistManager->isWatched( $user, $title ) );
|
|
$this->assertTrue( $watchlistManager->isTempWatched( $user, $title ) );
|
|
}
|
|
|
|
public function testEditUnwatch() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
$user = self::$users['sysop']->getUser();
|
|
$titleObj = Title::newFromText( $name );
|
|
|
|
$watchlistManager = $this->getServiceContainer()->getWatchlistManager();
|
|
$watchlistManager->addWatch( $user, $titleObj );
|
|
|
|
$this->assertFalse( $titleObj->exists() );
|
|
$this->assertTrue( $watchlistManager->isWatched( $user, $titleObj ) );
|
|
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'Some text',
|
|
'unwatch' => '',
|
|
] );
|
|
|
|
$this->assertTrue( $titleObj->exists() );
|
|
$this->assertFalse( $watchlistManager->isWatched( $user, $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_PRIMARY );
|
|
$this->assertSame( 'custom tag', $dbw->selectField(
|
|
[ 'change_tag', 'change_tag_def' ],
|
|
'ctd_name',
|
|
[ 'ct_rev_id' => $revId ],
|
|
__METHOD__,
|
|
[ 'change_tag_def' => [ 'JOIN', 'ctd_id = ct_tag_id' ] ]
|
|
)
|
|
);
|
|
}
|
|
|
|
public function testEditWithoutTagPermission() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
'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 testEditAbortedByEditPageHookWithResult() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->setTemporaryHook( 'EditFilterMergedContent',
|
|
static 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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
'The modification you tried to make was aborted by an extension.'
|
|
);
|
|
|
|
$this->setTemporaryHook( 'EditFilterMergedContent',
|
|
static 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->assertNull( DatabaseBlock::newFromTarget( '127.0.0.1' ), 'Sanity check' );
|
|
|
|
$block = new DatabaseBlock( [
|
|
'address' => self::$users['sysop']->getUser()->getName(),
|
|
'by' => self::$users['sysop']->getUser(),
|
|
'reason' => 'Capriciousness',
|
|
'timestamp' => '19370101000000',
|
|
'expiry' => 'infinity',
|
|
'enableAutoblock' => true,
|
|
] );
|
|
$blockStore = MediaWikiServices::getInstance()->getDatabaseBlockStore();
|
|
$blockStore->insertBlock( $block );
|
|
|
|
try {
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'Some text',
|
|
] );
|
|
$this->fail( 'Expected exception not thrown' );
|
|
} catch ( ApiUsageException $ex ) {
|
|
$this->assertSame( 'You have been blocked from editing.', $ex->getMessage() );
|
|
$this->assertNotNull( DatabaseBlock::newFromTarget( '127.0.0.1' ), 'Autoblock spread' );
|
|
} finally {
|
|
$blockStore->deleteBlock( $block );
|
|
self::$users['sysop']->getUser()->clearInstanceCache();
|
|
}
|
|
}
|
|
|
|
public function testEditWhileReadOnly() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( '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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage( "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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
'The content you supplied exceeds the article size limit of 1 kibibyte.'
|
|
);
|
|
|
|
$this->setMwGlobals( 'wgMaxArticleSize', 1 );
|
|
|
|
$text = str_repeat( '!', 1025 );
|
|
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => $text,
|
|
] );
|
|
}
|
|
|
|
public function testProhibitedAnonymousEdit() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
|
|
$this->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
// Two error messages are possible depending on the number of groups in the wiki with edit rights:
|
|
// - The action you have requested is limited to users in the group:
|
|
// - The action you have requested is limited to users in one of the groups:
|
|
'The action you have requested is limited to users in'
|
|
);
|
|
|
|
$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->expectException( ApiUsageException::class );
|
|
$this->expectExceptionMessage(
|
|
"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',
|
|
] );
|
|
}
|
|
|
|
public function testMidEditContentModelMismatch() {
|
|
$name = 'Help:' . ucfirst( __FUNCTION__ );
|
|
$title = Title::newFromText( $name );
|
|
|
|
$page = WikiPage::factory( $title );
|
|
|
|
// base edit, currently in Wikitext
|
|
$page->doUserEditContent(
|
|
new WikitextContent( "Foo" ),
|
|
self::$users['sysop']->getUser(),
|
|
"testing 1",
|
|
EDIT_NEW,
|
|
false
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101000000' );
|
|
$baseId = $page->getRevisionRecord()->getId();
|
|
|
|
// Attempt edit in Javascript. This may happen, for instance, if we
|
|
// started editing the base content while it was in Javascript and
|
|
// before we save it was changed to Wikitext (base edit model).
|
|
$page->doUserEditContent(
|
|
new JavaScriptContent( "Bar" ),
|
|
self::$users['uploader']->getUser(),
|
|
"testing 2",
|
|
EDIT_UPDATE,
|
|
$page->getLatest()
|
|
);
|
|
$this->forceRevisionDate( $page, '20120101020202' );
|
|
|
|
// ContentHanlder may throw exception if we attempt saving the above, so we will
|
|
// handle that with contentmodel-mismatch error. Test this is the case.
|
|
try {
|
|
$this->doApiRequestWithToken( [
|
|
'action' => 'edit',
|
|
'title' => $name,
|
|
'text' => 'different content models!',
|
|
'baserevid' => $baseId,
|
|
] );
|
|
$this->fail( "Should have raised an ApiUsageException" );
|
|
} catch ( ApiUsageException $e ) {
|
|
$this->assertTrue( self::apiExceptionHasCode( $e, 'contentmodel-mismatch' ) );
|
|
}
|
|
}
|
|
}
|