4 * Tests for MediaWiki api.php?action=edit.
6 * @author Daniel Kinzler
14 class ApiEditPageTest
extends ApiTestCase
{
16 protected function setUp() {
17 global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
21 $this->setContentLang( $wgContLang );
23 $this->setMwGlobals( [
24 'wgExtraNamespaces' => $wgExtraNamespaces,
25 'wgNamespaceContentModels' => $wgNamespaceContentModels,
26 'wgContentHandlers' => $wgContentHandlers,
29 $wgExtraNamespaces[12312] = 'Dummy';
30 $wgExtraNamespaces[12313] = 'Dummy_talk';
31 $wgExtraNamespaces[12314] = 'DummyNonText';
32 $wgExtraNamespaces[12315] = 'DummyNonText_talk';
34 $wgNamespaceContentModels[12312] = "testing";
35 $wgNamespaceContentModels[12314] = "testing-nontext";
37 $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
38 $wgContentHandlers["testing-nontext"] = 'DummyNonTextContentHandler';
39 $wgContentHandlers["testing-serialize-error"] =
40 'DummySerializeErrorContentHandler';
42 MWNamespace
::clearCaches();
43 $wgContLang->resetNamespaces(); # reset namespace cache
46 protected function tearDown() {
49 MWNamespace
::clearCaches();
50 $wgContLang->resetNamespaces(); # reset namespace cache
55 public function testEdit() {
56 $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext
58 // -- test new page --------------------------------------------
59 $apiResult = $this->doApiRequestWithToken( [
62 'text' => 'some text',
64 $apiResult = $apiResult[0];
66 // Validate API result data
67 $this->assertArrayHasKey( 'edit', $apiResult );
68 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
69 $this->assertSame( 'Success', $apiResult['edit']['result'] );
71 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
72 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
74 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
76 // -- test existing page, no change ----------------------------
77 $data = $this->doApiRequestWithToken( [
80 'text' => 'some text',
83 $this->assertSame( 'Success', $data[0]['edit']['result'] );
85 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
86 $this->assertArrayHasKey( 'nochange', $data[0]['edit'] );
88 // -- test existing page, with change --------------------------
89 $data = $this->doApiRequestWithToken( [
92 'text' => 'different text'
95 $this->assertSame( 'Success', $data[0]['edit']['result'] );
97 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
98 $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] );
100 $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] );
101 $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] );
102 $this->assertNotEquals(
103 $data[0]['edit']['newrevid'],
104 $data[0]['edit']['oldrevid'],
105 "revision id should change after edit"
112 public static function provideEditAppend() {
115 'foo', 'append', 'bar', "foobar"
118 'foo', 'prepend', 'bar', "barfoo"
120 [ # 2: append to empty page
121 '', 'append', 'foo', "foo"
123 [ # 3: prepend to empty page
124 '', 'prepend', 'foo', "foo"
126 [ # 4: append to non-existing page
127 null, 'append', 'foo', "foo"
129 [ # 5: prepend to non-existing page
130 null, 'prepend', 'foo', "foo"
136 * @dataProvider provideEditAppend
138 public function testEditAppend( $text, $op, $append, $expected ) {
142 // assume NS_HELP defaults to wikitext
143 $name = "Help:ApiEditPageTest_testEditAppend_$count";
145 // -- create page (or not) -----------------------------------------
146 if ( $text !== null ) {
147 list( $re ) = $this->doApiRequestWithToken( [
150 'text' => $text, ] );
152 $this->assertSame( 'Success', $re['edit']['result'] ); // sanity
155 // -- try append/prepend --------------------------------------------
156 list( $re ) = $this->doApiRequestWithToken( [
159 $op . 'text' => $append, ] );
161 $this->assertSame( 'Success', $re['edit']['result'] );
163 // -- validate -----------------------------------------------------
164 $page = new WikiPage( Title
::newFromText( $name ) );
165 $content = $page->getContent();
166 $this->assertNotNull( $content, 'Page should have been created' );
168 $text = $content->getNativeData();
170 $this->assertSame( $expected, $text );
174 * Test editing of sections
176 public function testEditSection() {
177 $name = 'Help:ApiEditPageTest_testEditSection';
178 $page = WikiPage
::factory( Title
::newFromText( $name ) );
179 $text = "==section 1==\ncontent 1\n==section 2==\ncontent2";
180 // Preload the page with some text
181 $page->doEditContent( ContentHandler
::makeContent( $text, $page->getTitle() ), 'summary' );
183 list( $re ) = $this->doApiRequestWithToken( [
187 'text' => "==section 1==\nnew content 1",
189 $this->assertSame( 'Success', $re['edit']['result'] );
190 $newtext = WikiPage
::factory( Title
::newFromText( $name ) )
191 ->getContent( Revision
::RAW
)
193 $this->assertSame( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext );
195 // Test that we raise a 'nosuchsection' error
197 $this->doApiRequestWithToken( [
203 $this->fail( "Should have raised an ApiUsageException" );
204 } catch ( ApiUsageException
$e ) {
205 $this->assertTrue( self
::apiExceptionHasCode( $e, 'nosuchsection' ) );
210 * Test action=edit§ion=new
211 * Run it twice so we test adding a new section on a
212 * page that doesn't exist (T54830) and one that
215 public function testEditNewSection() {
216 $name = 'Help:ApiEditPageTest_testEditNewSection';
218 // Test on a page that does not already exist
219 $this->assertFalse( Title
::newFromText( $name )->exists() );
220 list( $re ) = $this->doApiRequestWithToken( [
225 'summary' => 'header',
228 $this->assertSame( 'Success', $re['edit']['result'] );
229 // Check the page text is correct
230 $text = WikiPage
::factory( Title
::newFromText( $name ) )
231 ->getContent( Revision
::RAW
)
233 $this->assertSame( "== header ==\n\ntest", $text );
235 // Now on one that does
236 $this->assertTrue( Title
::newFromText( $name )->exists() );
237 list( $re2 ) = $this->doApiRequestWithToken( [
242 'summary' => 'header',
245 $this->assertSame( 'Success', $re2['edit']['result'] );
246 $text = WikiPage
::factory( Title
::newFromText( $name ) )
247 ->getContent( Revision
::RAW
)
249 $this->assertSame( "== header ==\n\ntest\n\n== header ==\n\ntest", $text );
253 * Ensure we can edit through a redirect, if adding a section
255 public function testEdit_redirect() {
259 // assume NS_HELP defaults to wikitext
260 $name = "Help:ApiEditPageTest_testEdit_redirect_$count";
261 $title = Title
::newFromText( $name );
262 $page = WikiPage
::factory( $title );
264 $rname = "Help:ApiEditPageTest_testEdit_redirect_r$count";
265 $rtitle = Title
::newFromText( $rname );
266 $rpage = WikiPage
::factory( $rtitle );
268 // base edit for content
269 $page->doEditContent( new WikitextContent( "Foo" ),
270 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
271 $this->forceRevisionDate( $page, '20120101000000' );
272 $baseTime = $page->getRevision()->getTimestamp();
274 // base edit for redirect
275 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
276 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
277 $this->forceRevisionDate( $rpage, '20120101000000' );
279 // conflicting edit to redirect
280 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
281 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
282 $this->forceRevisionDate( $rpage, '20120101020202' );
284 // try to save edit, following the redirect
285 list( $re, , ) = $this->doApiRequestWithToken( [
288 'text' => 'nix bar!',
289 'basetimestamp' => $baseTime,
294 $this->assertSame( 'Success', $re['edit']['result'],
295 "no problems expected when following redirect" );
299 * Ensure we cannot edit through a redirect, if attempting to overwrite content
301 public function testEdit_redirectText() {
305 // assume NS_HELP defaults to wikitext
306 $name = "Help:ApiEditPageTest_testEdit_redirectText_$count";
307 $title = Title
::newFromText( $name );
308 $page = WikiPage
::factory( $title );
310 $rname = "Help:ApiEditPageTest_testEdit_redirectText_r$count";
311 $rtitle = Title
::newFromText( $rname );
312 $rpage = WikiPage
::factory( $rtitle );
314 // base edit for content
315 $page->doEditContent( new WikitextContent( "Foo" ),
316 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
317 $this->forceRevisionDate( $page, '20120101000000' );
318 $baseTime = $page->getRevision()->getTimestamp();
320 // base edit for redirect
321 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
322 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
323 $this->forceRevisionDate( $rpage, '20120101000000' );
325 // conflicting edit to redirect
326 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
327 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
328 $this->forceRevisionDate( $rpage, '20120101020202' );
330 // try to save edit, following the redirect but without creating a section
332 $this->doApiRequestWithToken( [
335 'text' => 'nix bar!',
336 'basetimestamp' => $baseTime,
340 $this->fail( 'redirect-appendonly error expected' );
341 } catch ( ApiUsageException
$ex ) {
342 $this->assertTrue( self
::apiExceptionHasCode( $ex, 'redirect-appendonly' ) );
346 public function testEditConflict() {
350 // assume NS_HELP defaults to wikitext
351 $name = "Help:ApiEditPageTest_testEditConflict_$count";
352 $title = Title
::newFromText( $name );
354 $page = WikiPage
::factory( $title );
357 $page->doEditContent( new WikitextContent( "Foo" ),
358 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
359 $this->forceRevisionDate( $page, '20120101000000' );
360 $baseTime = $page->getRevision()->getTimestamp();
363 $page->doEditContent( new WikitextContent( "Foo bar" ),
364 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
365 $this->forceRevisionDate( $page, '20120101020202' );
367 // try to save edit, expect conflict
369 $this->doApiRequestWithToken( [
372 'text' => 'nix bar!',
373 'basetimestamp' => $baseTime,
376 $this->fail( 'edit conflict expected' );
377 } catch ( ApiUsageException
$ex ) {
378 $this->assertTrue( self
::apiExceptionHasCode( $ex, 'editconflict' ) );
383 * Ensure that editing using section=new will prevent simple conflicts
385 public function testEditConflict_newSection() {
389 // assume NS_HELP defaults to wikitext
390 $name = "Help:ApiEditPageTest_testEditConflict_newSection_$count";
391 $title = Title
::newFromText( $name );
393 $page = WikiPage
::factory( $title );
396 $page->doEditContent( new WikitextContent( "Foo" ),
397 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
398 $this->forceRevisionDate( $page, '20120101000000' );
399 $baseTime = $page->getRevision()->getTimestamp();
402 $page->doEditContent( new WikitextContent( "Foo bar" ),
403 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
404 $this->forceRevisionDate( $page, '20120101020202' );
406 // try to save edit, expect no conflict
407 list( $re, , ) = $this->doApiRequestWithToken( [
410 'text' => 'nix bar!',
411 'basetimestamp' => $baseTime,
415 $this->assertSame( 'Success', $re['edit']['result'],
416 "no edit conflict expected here" );
419 public function testEditConflict_bug41990() {
424 * T43990: if the target page has a newer revision than the redirect, then editing the
425 * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously
426 * caused an edit conflict to be detected.
429 // assume NS_HELP defaults to wikitext
430 $name = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_$count";
431 $title = Title
::newFromText( $name );
432 $page = WikiPage
::factory( $title );
434 $rname = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_r$count";
435 $rtitle = Title
::newFromText( $rname );
436 $rpage = WikiPage
::factory( $rtitle );
438 // base edit for content
439 $page->doEditContent( new WikitextContent( "Foo" ),
440 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
441 $this->forceRevisionDate( $page, '20120101000000' );
443 // base edit for redirect
444 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
445 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
446 $this->forceRevisionDate( $rpage, '20120101000000' );
448 // new edit to content
449 $page->doEditContent( new WikitextContent( "Foo bar" ),
450 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
451 $this->forceRevisionDate( $rpage, '20120101020202' );
453 // try to save edit; should work, following the redirect.
454 list( $re, , ) = $this->doApiRequestWithToken( [
457 'text' => 'nix bar!',
462 $this->assertSame( 'Success', $re['edit']['result'],
463 "no edit conflict expected here" );
467 * @param WikiPage $page
468 * @param string|int $timestamp
470 protected function forceRevisionDate( WikiPage
$page, $timestamp ) {
471 $dbw = wfGetDB( DB_MASTER
);
473 $dbw->update( 'revision',
474 [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
475 [ 'rev_id' => $page->getLatest() ] );
480 public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
481 $this->setExpectedException(
482 ApiUsageException
::class,
483 'Direct editing via API is not supported for content model ' .
484 'testing used by Dummy:ApiEditPageTest_nonTextPageEdit'
487 $this->doApiRequestWithToken( [
489 'title' => 'Dummy:ApiEditPageTest_nonTextPageEdit',
490 'text' => '{"animals":["kittens!"]}'
494 public function testSupportsDirectApiEditing_withContentHandlerOverride() {
495 $name = 'DummyNonText:ApiEditPageTest_testNonTextEdit';
496 $data = serialize( 'some bla bla text' );
498 $result = $this->doApiRequestWithToken( [
504 $apiResult = $result[0];
506 // Validate API result data
507 $this->assertArrayHasKey( 'edit', $apiResult );
508 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
509 $this->assertSame( 'Success', $apiResult['edit']['result'] );
511 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
512 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
514 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
516 // validate resulting revision
517 $page = WikiPage
::factory( Title
::newFromText( $name ) );
518 $this->assertSame( "testing-nontext", $page->getContentModel() );
519 $this->assertSame( $data, $page->getContent()->serialize() );
523 * This test verifies that after changing the content model
524 * of a page, undoing that edit via the API will also
525 * undo the content model change.
527 public function testUndoAfterContentModelChange() {
528 $name = 'Help:' . __FUNCTION__
;
529 $uploader = self
::$users['uploader']->getUser();
530 $sysop = self
::$users['sysop']->getUser();
532 $apiResult = $this->doApiRequestWithToken( [
535 'text' => 'some text',
536 ], null, $sysop )[0];
539 $this->assertArrayHasKey( 'edit', $apiResult );
540 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
541 $this->assertSame( 'Success', $apiResult['edit']['result'] );
542 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
543 // Content model is wikitext
544 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
546 // Convert the page to JSON
547 $apiResult = $this->doApiRequestWithToken( [
551 'contentmodel' => 'json',
552 ], null, $uploader )[0];
555 $this->assertArrayHasKey( 'edit', $apiResult );
556 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
557 $this->assertSame( 'Success', $apiResult['edit']['result'] );
558 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
559 $this->assertSame( 'json', $apiResult['edit']['contentmodel'] );
561 $apiResult = $this->doApiRequestWithToken( [
564 'undo' => $apiResult['edit']['newrevid']
565 ], null, $sysop )[0];
568 $this->assertArrayHasKey( 'edit', $apiResult );
569 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
570 $this->assertSame( 'Success', $apiResult['edit']['result'] );
571 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
572 // Check that the contentmodel is back to wikitext now.
573 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
576 // The tests below are mostly not commented because they do exactly what
577 // you'd expect from the name.
579 public function testCorrectContentFormat() {
580 $name = 'Help:' . ucfirst( __FUNCTION__
);
582 $this->doApiRequestWithToken( [
585 'text' => 'some text',
586 'contentmodel' => 'wikitext',
587 'contentformat' => 'text/x-wiki',
590 $this->assertTrue( Title
::newFromText( $name )->exists() );
593 public function testUnsupportedContentFormat() {
594 $name = 'Help:' . ucfirst( __FUNCTION__
);
596 $this->setExpectedException( ApiUsageException
::class,
597 'Unrecognized value for parameter "contentformat": nonexistent format.' );
600 $this->doApiRequestWithToken( [
603 'text' => 'some text',
604 'contentformat' => 'nonexistent format',
607 $this->assertFalse( Title
::newFromText( $name )->exists() );
611 public function testMismatchedContentFormat() {
612 $name = 'Help:' . ucfirst( __FUNCTION__
);
614 $this->setExpectedException( ApiUsageException
::class,
615 'The requested format text/plain is not supported for content ' .
616 "model wikitext used by $name." );
619 $this->doApiRequestWithToken( [
622 'text' => 'some text',
623 'contentmodel' => 'wikitext',
624 'contentformat' => 'text/plain',
627 $this->assertFalse( Title
::newFromText( $name )->exists() );
631 public function testUndoToInvalidRev() {
632 $name = 'Help:' . ucfirst( __FUNCTION__
);
634 $revId = $this->editPage( $name, 'Some text' )->value
['revision']
638 $this->setExpectedException( ApiUsageException
::class,
639 "There is no revision with ID $revId." );
641 $this->doApiRequestWithToken( [
649 * Tests what happens if the undo parameter is a valid revision, but
650 * the undoafter parameter doesn't refer to a revision that exists in the
653 public function testUndoAfterToInvalidRev() {
654 // We can't just pick a large number for undoafter (as in
655 // testUndoToInvalidRev above), because then MediaWiki will helpfully
656 // assume we switched around undo and undoafter and we'll test the code
657 // path for undo being invalid, not undoafter. So instead we delete
658 // the revision from the database. In real life this case could come
659 // up if a revision number was skipped, e.g., if two transactions try
660 // to insert new revision rows at once and the first one to succeed
662 $name = 'Help:' . ucfirst( __FUNCTION__
);
663 $titleObj = Title
::newFromText( $name );
665 $revId1 = $this->editPage( $name, '1' )->value
['revision']->getId();
666 $revId2 = $this->editPage( $name, '2' )->value
['revision']->getId();
667 $revId3 = $this->editPage( $name, '3' )->value
['revision']->getId();
669 // Make the middle revision disappear
670 $dbw = wfGetDB( DB_MASTER
);
671 $dbw->delete( 'revision', [ 'rev_id' => $revId2 ], __METHOD__
);
672 $dbw->update( 'revision', [ 'rev_parent_id' => $revId1 ],
673 [ 'rev_id' => $revId3 ], __METHOD__
);
675 $this->setExpectedException( ApiUsageException
::class,
676 "There is no revision with ID $revId2." );
678 $this->doApiRequestWithToken( [
682 'undoafter' => $revId2,
687 * Tests what happens if the undo parameter is a valid revision, but
688 * undoafter is hidden (rev_deleted).
690 public function testUndoAfterToHiddenRev() {
691 $name = 'Help:' . ucfirst( __FUNCTION__
);
692 $titleObj = Title
::newFromText( $name );
694 $this->editPage( $name, '0' );
696 $revId1 = $this->editPage( $name, '1' )->value
['revision']->getId();
698 $revId2 = $this->editPage( $name, '2' )->value
['revision']->getId();
700 // Hide the middle revision
701 $list = RevisionDeleter
::createList( 'revision',
702 RequestContext
::getMain(), $titleObj, [ $revId1 ] );
703 $list->setVisibility( [
704 'value' => [ Revision
::DELETED_TEXT
=> 1 ],
705 'comment' => 'Bye-bye',
708 $this->setExpectedException( ApiUsageException
::class,
709 "There is no revision with ID $revId1." );
711 $this->doApiRequestWithToken( [
715 'undoafter' => $revId1,
720 * Test undo when a revision with a higher id has an earlier timestamp.
721 * This can happen if importing an old revision.
723 public function testUndoWithSwappedRevisions() {
724 $name = 'Help:' . ucfirst( __FUNCTION__
);
725 $titleObj = Title
::newFromText( $name );
727 $this->editPage( $name, '0' );
729 $revId2 = $this->editPage( $name, '2' )->value
['revision']->getId();
731 $revId1 = $this->editPage( $name, '1' )->value
['revision']->getId();
733 // Now monkey with the timestamp
734 $dbw = wfGetDB( DB_MASTER
);
737 [ 'rev_timestamp' => $dbw->timestamp( time() - 86400 ) ],
738 [ 'rev_id' => $revId1 ],
742 $this->doApiRequestWithToken( [
746 'undoafter' => $revId1,
749 $text = ( new WikiPage( $titleObj ) )->getContent()->getNativeData();
751 // This is wrong! It should be 1. But let's test for our incorrect
752 // behavior for now, so if someone fixes it they'll fix the test as
753 // well to expect 1. If we disabled the test, it might stay disabled
754 // even once the bug is fixed, which would be a shame.
755 $this->assertSame( '2', $text );
758 public function testUndoWithConflicts() {
759 $name = 'Help:' . ucfirst( __FUNCTION__
);
761 $this->setExpectedException( ApiUsageException
::class,
762 'The edit could not be undone due to conflicting intermediate edits.' );
764 $this->editPage( $name, '1' );
766 $revId = $this->editPage( $name, '2' )->value
['revision']->getId();
768 $this->editPage( $name, '3' );
770 $this->doApiRequestWithToken( [
776 $text = ( new WikiPage( Title
::newFromText( $name ) ) )->getContent()
778 $this->assertSame( '3', $text );
782 * undoafter is supposed to be less than undo. If not, we reverse their
783 * meaning, so that the two are effectively interchangeable.
785 public function testReversedUndoAfter() {
786 $name = 'Help:' . ucfirst( __FUNCTION__
);
788 $this->editPage( $name, '0' );
789 $revId1 = $this->editPage( $name, '1' )->value
['revision']->getId();
790 $revId2 = $this->editPage( $name, '2' )->value
['revision']->getId();
792 $this->doApiRequestWithToken( [
796 'undoafter' => $revId2,
799 $text = ( new WikiPage( Title
::newFromText( $name ) ) )->getContent()
801 $this->assertSame( '1', $text );
804 public function testUndoToRevFromDifferentPage() {
805 $name = 'Help:' . ucfirst( __FUNCTION__
);
807 $this->editPage( "$name-1", 'Some text' );
808 $revId = $this->editPage( "$name-1", 'Some more text' )
809 ->value
['revision']->getId();
811 $this->editPage( "$name-2", 'Some text' );
813 $this->setExpectedException( ApiUsageException
::class,
814 "r$revId is not a revision of $name-2." );
816 $this->doApiRequestWithToken( [
818 'title' => "$name-2",
823 public function testUndoAfterToRevFromDifferentPage() {
824 $name = 'Help:' . ucfirst( __FUNCTION__
);
826 $revId1 = $this->editPage( "$name-1", 'Some text' )
827 ->value
['revision']->getId();
829 $revId2 = $this->editPage( "$name-2", 'Some text' )
830 ->value
['revision']->getId();
832 $this->setExpectedException( ApiUsageException
::class,
833 "r$revId1 is not a revision of $name-2." );
835 $this->doApiRequestWithToken( [
837 'title' => "$name-2",
839 'undoafter' => $revId1,
843 public function testMd5Text() {
844 $name = 'Help:' . ucfirst( __FUNCTION__
);
846 $this->assertFalse( Title
::newFromText( $name )->exists() );
848 $this->doApiRequestWithToken( [
851 'text' => 'Some text',
852 'md5' => md5( 'Some text' ),
855 $this->assertTrue( Title
::newFromText( $name )->exists() );
858 public function testMd5PrependText() {
859 $name = 'Help:' . ucfirst( __FUNCTION__
);
861 $this->editPage( $name, 'Some text' );
863 $this->doApiRequestWithToken( [
866 'prependtext' => 'Alert: ',
867 'md5' => md5( 'Alert: ' ),
870 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
871 ->getContent()->getNativeData();
872 $this->assertSame( 'Alert: Some text', $text );
875 public function testMd5AppendText() {
876 $name = 'Help:' . ucfirst( __FUNCTION__
);
878 $this->editPage( $name, 'Some text' );
880 $this->doApiRequestWithToken( [
883 'appendtext' => ' is nice',
884 'md5' => md5( ' is nice' ),
887 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
888 ->getContent()->getNativeData();
889 $this->assertSame( 'Some text is nice', $text );
892 public function testMd5PrependAndAppendText() {
893 $name = 'Help:' . ucfirst( __FUNCTION__
);
895 $this->editPage( $name, 'Some text' );
897 $this->doApiRequestWithToken( [
900 'prependtext' => 'Alert: ',
901 'appendtext' => ' is nice',
902 'md5' => md5( 'Alert: is nice' ),
905 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
906 ->getContent()->getNativeData();
907 $this->assertSame( 'Alert: Some text is nice', $text );
910 public function testIncorrectMd5Text() {
911 $name = 'Help:' . ucfirst( __FUNCTION__
);
913 $this->setExpectedException( ApiUsageException
::class,
914 'The supplied MD5 hash was incorrect.' );
916 $this->doApiRequestWithToken( [
919 'text' => 'Some text',
924 public function testIncorrectMd5PrependText() {
925 $name = 'Help:' . ucfirst( __FUNCTION__
);
927 $this->setExpectedException( ApiUsageException
::class,
928 'The supplied MD5 hash was incorrect.' );
930 $this->doApiRequestWithToken( [
933 'prependtext' => 'Some ',
934 'appendtext' => 'text',
935 'md5' => md5( 'Some ' ),
939 public function testIncorrectMd5AppendText() {
940 $name = 'Help:' . ucfirst( __FUNCTION__
);
942 $this->setExpectedException( ApiUsageException
::class,
943 'The supplied MD5 hash was incorrect.' );
945 $this->doApiRequestWithToken( [
948 'prependtext' => 'Some ',
949 'appendtext' => 'text',
950 'md5' => md5( 'text' ),
954 public function testCreateOnly() {
955 $name = 'Help:' . ucfirst( __FUNCTION__
);
957 $this->setExpectedException( ApiUsageException
::class,
958 'The article you tried to create has been created already.' );
960 $this->editPage( $name, 'Some text' );
961 $this->assertTrue( Title
::newFromText( $name )->exists() );
964 $this->doApiRequestWithToken( [
967 'text' => 'Some more text',
971 // Validate that content was not changed
972 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
973 ->getContent()->getNativeData();
975 $this->assertSame( 'Some text', $text );
979 public function testNoCreate() {
980 $name = 'Help:' . ucfirst( __FUNCTION__
);
982 $this->setExpectedException( ApiUsageException
::class,
983 "The page you specified doesn't exist." );
985 $this->assertFalse( Title
::newFromText( $name )->exists() );
988 $this->doApiRequestWithToken( [
991 'text' => 'Some text',
995 $this->assertFalse( Title
::newFromText( $name )->exists() );
1000 * Appending/prepending is currently only supported for TextContent. We
1001 * test this right now, and when support is added this test should be
1002 * replaced by tests that the support is correct.
1004 public function testAppendWithNonTextContentHandler() {
1005 $name = 'MediaWiki:' . ucfirst( __FUNCTION__
);
1007 $this->setExpectedException( ApiUsageException
::class,
1008 "Can't append to pages using content model testing-nontext." );
1010 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1011 function ( Title
$title, &$model ) use ( $name ) {
1012 if ( $title->getPrefixedText() === $name ) {
1013 $model = 'testing-nontext';
1019 $this->doApiRequestWithToken( [
1022 'appendtext' => 'Some text',
1026 public function testAppendInMediaWikiNamespace() {
1027 $name = 'MediaWiki:' . ucfirst( __FUNCTION__
);
1029 $this->assertFalse( Title
::newFromText( $name )->exists() );
1031 $this->doApiRequestWithToken( [
1034 'appendtext' => 'Some text',
1037 $this->assertTrue( Title
::newFromText( $name )->exists() );
1040 public function testAppendInMediaWikiNamespaceWithSerializationError() {
1041 $name = 'MediaWiki:' . ucfirst( __FUNCTION__
);
1043 $this->setExpectedException( ApiUsageException
::class,
1044 'Content serialization failed: Could not unserialize content' );
1046 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1047 function ( Title
$title, &$model ) use ( $name ) {
1048 if ( $title->getPrefixedText() === $name ) {
1049 $model = 'testing-serialize-error';
1055 $this->doApiRequestWithToken( [
1058 'appendtext' => 'Some text',
1062 public function testAppendNewSection() {
1063 $name = 'Help:' . ucfirst( __FUNCTION__
);
1065 $this->editPage( $name, 'Initial content' );
1067 $this->doApiRequestWithToken( [
1070 'appendtext' => '== New section ==',
1074 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1075 ->getContent()->getNativeData();
1077 $this->assertSame( "Initial content\n\n== New section ==", $text );
1080 public function testAppendNewSectionWithInvalidContentModel() {
1081 $name = 'Help:' . ucfirst( __FUNCTION__
);
1083 $this->setExpectedException( ApiUsageException
::class,
1084 'Sections are not supported for content model text.' );
1086 $this->editPage( $name, 'Initial content' );
1088 $this->doApiRequestWithToken( [
1091 'appendtext' => '== New section ==',
1093 'contentmodel' => 'text',
1097 public function testAppendNewSectionWithTitle() {
1098 $name = 'Help:' . ucfirst( __FUNCTION__
);
1100 $this->editPage( $name, 'Initial content' );
1102 $this->doApiRequestWithToken( [
1105 'sectiontitle' => 'My section',
1106 'appendtext' => 'More content',
1110 $page = new WikiPage( Title
::newFromText( $name ) );
1112 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1113 $page->getContent()->getNativeData() );
1114 $this->assertSame( '/* My section */ new section',
1115 $page->getRevision()->getComment() );
1118 public function testAppendNewSectionWithSummary() {
1119 $name = 'Help:' . ucfirst( __FUNCTION__
);
1121 $this->editPage( $name, 'Initial content' );
1123 $this->doApiRequestWithToken( [
1126 'appendtext' => 'More content',
1128 'summary' => 'Add new section',
1131 $page = new WikiPage( Title
::newFromText( $name ) );
1133 $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content",
1134 $page->getContent()->getNativeData() );
1135 // EditPage actually assumes the summary is the section name here
1136 $this->assertSame( '/* Add new section */ new section',
1137 $page->getRevision()->getComment() );
1140 public function testAppendNewSectionWithTitleAndSummary() {
1141 $name = 'Help:' . ucfirst( __FUNCTION__
);
1143 $this->editPage( $name, 'Initial content' );
1145 $this->doApiRequestWithToken( [
1148 'sectiontitle' => 'My section',
1149 'appendtext' => 'More content',
1151 'summary' => 'Add new section',
1154 $page = new WikiPage( Title
::newFromText( $name ) );
1156 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1157 $page->getContent()->getNativeData() );
1158 $this->assertSame( 'Add new section',
1159 $page->getRevision()->getComment() );
1162 public function testAppendToSection() {
1163 $name = 'Help:' . ucfirst( __FUNCTION__
);
1165 $this->editPage( $name, "== Section 1 ==\n\nContent\n\n" .
1166 "== Section 2 ==\n\nFascinating!" );
1168 $this->doApiRequestWithToken( [
1171 'appendtext' => ' and more content',
1175 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1176 ->getContent()->getNativeData();
1178 $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" .
1179 "== Section 2 ==\n\nFascinating!", $text );
1182 public function testAppendToFirstSection() {
1183 $name = 'Help:' . ucfirst( __FUNCTION__
);
1185 $this->editPage( $name, "Content\n\n== Section 1 ==\n\nFascinating!" );
1187 $this->doApiRequestWithToken( [
1190 'appendtext' => ' and more content',
1194 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1195 ->getContent()->getNativeData();
1197 $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" .
1198 "Fascinating!", $text );
1201 public function testAppendToNonexistentSection() {
1202 $name = 'Help:' . ucfirst( __FUNCTION__
);
1204 $this->setExpectedException( ApiUsageException
::class, 'There is no section 1.' );
1206 $this->editPage( $name, 'Content' );
1209 $this->doApiRequestWithToken( [
1212 'appendtext' => ' and more content',
1216 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1217 ->getContent()->getNativeData();
1219 $this->assertSame( 'Content', $text );
1223 public function testEditMalformedSection() {
1224 $name = 'Help:' . ucfirst( __FUNCTION__
);
1226 $this->setExpectedException( ApiUsageException
::class,
1227 'The "section" parameter must be a valid section ID or "new".' );
1228 $this->editPage( $name, 'Content' );
1231 $this->doApiRequestWithToken( [
1234 'text' => 'Different content',
1235 'section' => 'It is unlikely that this is valid',
1238 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1239 ->getContent()->getNativeData();
1241 $this->assertSame( 'Content', $text );
1245 public function testEditWithStartTimestamp() {
1246 $name = 'Help:' . ucfirst( __FUNCTION__
);
1247 $this->setExpectedException( ApiUsageException
::class,
1248 'The page has been deleted since you fetched its timestamp.' );
1250 $startTime = MWTimestamp
::convert( TS_MW
, time() - 1 );
1252 $this->editPage( $name, 'Some text' );
1254 $pageObj = new WikiPage( Title
::newFromText( $name ) );
1255 $pageObj->doDeleteArticle( 'Bye-bye' );
1257 $this->assertFalse( $pageObj->exists() );
1260 $this->doApiRequestWithToken( [
1263 'text' => 'Different text',
1264 'starttimestamp' => $startTime,
1267 $this->assertFalse( $pageObj->exists() );
1271 public function testEditMinor() {
1272 $name = 'Help:' . ucfirst( __FUNCTION__
);
1274 $this->editPage( $name, 'Some text' );
1276 $this->doApiRequestWithToken( [
1279 'text' => 'Different text',
1283 $revisionStore = \MediaWiki\MediaWikiServices
::getInstance()->getRevisionStore();
1284 $revision = $revisionStore->getRevisionByTitle( Title
::newFromText( $name ) );
1285 $this->assertTrue( $revision->isMinor() );
1288 public function testEditRecreate() {
1289 $name = 'Help:' . ucfirst( __FUNCTION__
);
1291 $startTime = MWTimestamp
::convert( TS_MW
, time() - 1 );
1293 $this->editPage( $name, 'Some text' );
1295 $pageObj = new WikiPage( Title
::newFromText( $name ) );
1296 $pageObj->doDeleteArticle( 'Bye-bye' );
1298 $this->assertFalse( $pageObj->exists() );
1300 $this->doApiRequestWithToken( [
1303 'text' => 'Different text',
1304 'starttimestamp' => $startTime,
1308 $this->assertTrue( Title
::newFromText( $name )->exists() );
1311 public function testEditWatch() {
1312 $name = 'Help:' . ucfirst( __FUNCTION__
);
1313 $user = self
::$users['sysop']->getUser();
1315 $this->doApiRequestWithToken( [
1318 'text' => 'Some text',
1322 $this->assertTrue( Title
::newFromText( $name )->exists() );
1323 $this->assertTrue( $user->isWatched( Title
::newFromText( $name ) ) );
1326 public function testEditUnwatch() {
1327 $name = 'Help:' . ucfirst( __FUNCTION__
);
1328 $user = self
::$users['sysop']->getUser();
1329 $titleObj = Title
::newFromText( $name );
1331 $user->addWatch( $titleObj );
1333 $this->assertFalse( $titleObj->exists() );
1334 $this->assertTrue( $user->isWatched( $titleObj ) );
1336 $this->doApiRequestWithToken( [
1339 'text' => 'Some text',
1343 $this->assertTrue( $titleObj->exists() );
1344 $this->assertFalse( $user->isWatched( $titleObj ) );
1347 public function testEditWithTag() {
1348 $name = 'Help:' . ucfirst( __FUNCTION__
);
1350 ChangeTags
::defineTag( 'custom tag' );
1352 $revId = $this->doApiRequestWithToken( [
1355 'text' => 'Some text',
1356 'tags' => 'custom tag',
1357 ] )[0]['edit']['newrevid'];
1359 $dbw = wfGetDB( DB_MASTER
);
1360 $this->assertSame( 'custom tag', $dbw->selectField(
1361 'change_tag', 'ct_tag', [ 'ct_rev_id' => $revId ], __METHOD__
) );
1364 public function testEditWithoutTagPermission() {
1365 $name = 'Help:' . ucfirst( __FUNCTION__
);
1367 $this->setExpectedException( ApiUsageException
::class,
1368 'You do not have permission to apply change tags along with your changes.' );
1370 $this->assertFalse( Title
::newFromText( $name )->exists() );
1372 ChangeTags
::defineTag( 'custom tag' );
1373 $this->setMwGlobals( 'wgRevokePermissions',
1374 [ 'user' => [ 'applychangetags' => true ] ] );
1376 $this->doApiRequestWithToken( [
1379 'text' => 'Some text',
1380 'tags' => 'custom tag',
1383 $this->assertFalse( Title
::newFromText( $name )->exists() );
1387 public function testEditAbortedByHook() {
1388 $name = 'Help:' . ucfirst( __FUNCTION__
);
1390 $this->setExpectedException( ApiUsageException
::class,
1391 'The modification you tried to make was aborted by an extension.' );
1393 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1394 'hook-APIEditBeforeSave-closure)' );
1396 $this->setTemporaryHook( 'APIEditBeforeSave',
1403 $this->doApiRequestWithToken( [
1406 'text' => 'Some text',
1409 $this->assertFalse( Title
::newFromText( $name )->exists() );
1413 public function testEditAbortedByHookWithCustomOutput() {
1414 $name = 'Help:' . ucfirst( __FUNCTION__
);
1416 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1417 'hook-APIEditBeforeSave-closure)' );
1419 $this->setTemporaryHook( 'APIEditBeforeSave',
1420 function ( $unused1, $unused2, &$r ) {
1421 $r['msg'] = 'Some message';
1425 $result = $this->doApiRequestWithToken( [
1428 'text' => 'Some text',
1430 Wikimedia\restoreWarnings
();
1432 $this->assertSame( [ 'msg' => 'Some message', 'result' => 'Failure' ],
1433 $result[0]['edit'] );
1435 $this->assertFalse( Title
::newFromText( $name )->exists() );
1438 public function testEditAbortedByEditPageHookWithResult() {
1439 $name = 'Help:' . ucfirst( __FUNCTION__
);
1441 $this->setTemporaryHook( 'EditFilterMergedContent',
1442 function ( $unused1, $unused2, Status
$status ) {
1443 $status->apiHookResult
= [ 'msg' => 'A message for you!' ];
1447 $res = $this->doApiRequestWithToken( [
1450 'text' => 'Some text',
1453 $this->assertFalse( Title
::newFromText( $name )->exists() );
1454 $this->assertSame( [ 'edit' => [ 'msg' => 'A message for you!',
1455 'result' => 'Failure' ] ], $res[0] );
1458 public function testEditAbortedByEditPageHookWithNoResult() {
1459 $name = 'Help:' . ucfirst( __FUNCTION__
);
1461 $this->setExpectedException( ApiUsageException
::class,
1462 'The modification you tried to make was aborted by an extension.' );
1464 $this->setTemporaryHook( 'EditFilterMergedContent',
1471 $this->doApiRequestWithToken( [
1474 'text' => 'Some text',
1477 $this->assertFalse( Title
::newFromText( $name )->exists() );
1481 public function testEditWhileBlocked() {
1482 $name = 'Help:' . ucfirst( __FUNCTION__
);
1484 $this->setExpectedException( ApiUsageException
::class,
1485 'You have been blocked from editing.' );
1487 $block = new Block( [
1488 'address' => self
::$users['sysop']->getUser()->getName(),
1489 'by' => self
::$users['sysop']->getUser()->getId(),
1490 'reason' => 'Capriciousness',
1491 'timestamp' => '19370101000000',
1492 'expiry' => 'infinity',
1497 $this->doApiRequestWithToken( [
1500 'text' => 'Some text',
1504 self
::$users['sysop']->getUser()->clearInstanceCache();
1508 public function testEditWhileReadOnly() {
1509 $name = 'Help:' . ucfirst( __FUNCTION__
);
1511 $this->setExpectedException( ApiUsageException
::class,
1512 'The wiki is currently in read-only mode.' );
1514 $svc = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1515 $svc->setReason( "Read-only for testing" );
1518 $this->doApiRequestWithToken( [
1521 'text' => 'Some text',
1524 $svc->setReason( false );
1528 public function testCreateImageRedirectAnon() {
1529 $name = 'File:' . ucfirst( __FUNCTION__
);
1531 $this->setExpectedException( ApiUsageException
::class,
1532 "Anonymous users can't create image redirects." );
1534 $this->doApiRequestWithToken( [
1537 'text' => '#REDIRECT [[File:Other file.png]]',
1538 ], null, new User() );
1541 public function testCreateImageRedirectLoggedIn() {
1542 $name = 'File:' . ucfirst( __FUNCTION__
);
1544 $this->setExpectedException( ApiUsageException
::class,
1545 "You don't have permission to create image redirects." );
1547 $this->setMwGlobals( 'wgRevokePermissions',
1548 [ 'user' => [ 'upload' => true ] ] );
1550 $this->doApiRequestWithToken( [
1553 'text' => '#REDIRECT [[File:Other file.png]]',
1557 public function testTooBigEdit() {
1558 $name = 'Help:' . ucfirst( __FUNCTION__
);
1560 $this->setExpectedException( ApiUsageException
::class,
1561 'The content you supplied exceeds the article size limit of 1 kilobyte.' );
1563 $this->setMwGlobals( 'wgMaxArticleSize', 1 );
1565 $text = str_repeat( '!', 1025 );
1567 $this->doApiRequestWithToken( [
1574 public function testProhibitedAnonymousEdit() {
1575 $name = 'Help:' . ucfirst( __FUNCTION__
);
1577 $this->setExpectedException( ApiUsageException
::class,
1578 'The action you have requested is limited to users in the group: ' );
1580 $this->setMwGlobals( 'wgRevokePermissions', [ '*' => [ 'edit' => true ] ] );
1582 $this->doApiRequestWithToken( [
1585 'text' => 'Some text',
1586 ], null, new User() );
1589 public function testProhibitedChangeContentModel() {
1590 $name = 'Help:' . ucfirst( __FUNCTION__
);
1592 $this->setExpectedException( ApiUsageException
::class,
1593 "You don't have permission to change the content model of a page." );
1595 $this->setMwGlobals( 'wgRevokePermissions',
1596 [ 'user' => [ 'editcontentmodel' => true ] ] );
1598 $this->doApiRequestWithToken( [
1601 'text' => 'Some text',
1602 'contentmodel' => 'json',