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->setMwGlobals( [
22 'wgExtraNamespaces' => $wgExtraNamespaces,
23 'wgNamespaceContentModels' => $wgNamespaceContentModels,
24 'wgContentHandlers' => $wgContentHandlers,
25 'wgContLang' => $wgContLang,
28 $wgExtraNamespaces[12312] = 'Dummy';
29 $wgExtraNamespaces[12313] = 'Dummy_talk';
30 $wgExtraNamespaces[12314] = 'DummyNonText';
31 $wgExtraNamespaces[12315] = 'DummyNonText_talk';
33 $wgNamespaceContentModels[12312] = "testing";
34 $wgNamespaceContentModels[12314] = "testing-nontext";
36 $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
37 $wgContentHandlers["testing-nontext"] = 'DummyNonTextContentHandler';
38 $wgContentHandlers["testing-serialize-error"] =
39 'DummySerializeErrorContentHandler';
41 MWNamespace
::clearCaches();
42 $wgContLang->resetNamespaces(); # reset namespace cache
47 protected function tearDown() {
50 MWNamespace
::clearCaches();
51 $wgContLang->resetNamespaces(); # reset namespace cache
56 public function testEdit() {
57 $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext
59 // -- test new page --------------------------------------------
60 $apiResult = $this->doApiRequestWithToken( [
63 'text' => 'some text',
65 $apiResult = $apiResult[0];
67 // Validate API result data
68 $this->assertArrayHasKey( 'edit', $apiResult );
69 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
70 $this->assertSame( 'Success', $apiResult['edit']['result'] );
72 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
73 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
75 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
77 // -- test existing page, no change ----------------------------
78 $data = $this->doApiRequestWithToken( [
81 'text' => 'some text',
84 $this->assertSame( 'Success', $data[0]['edit']['result'] );
86 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
87 $this->assertArrayHasKey( 'nochange', $data[0]['edit'] );
89 // -- test existing page, with change --------------------------
90 $data = $this->doApiRequestWithToken( [
93 'text' => 'different text'
96 $this->assertSame( 'Success', $data[0]['edit']['result'] );
98 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
99 $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] );
101 $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] );
102 $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] );
103 $this->assertNotEquals(
104 $data[0]['edit']['newrevid'],
105 $data[0]['edit']['oldrevid'],
106 "revision id should change after edit"
113 public static function provideEditAppend() {
116 'foo', 'append', 'bar', "foobar"
119 'foo', 'prepend', 'bar', "barfoo"
121 [ # 2: append to empty page
122 '', 'append', 'foo', "foo"
124 [ # 3: prepend to empty page
125 '', 'prepend', 'foo', "foo"
127 [ # 4: append to non-existing page
128 null, 'append', 'foo', "foo"
130 [ # 5: prepend to non-existing page
131 null, 'prepend', 'foo', "foo"
137 * @dataProvider provideEditAppend
139 public function testEditAppend( $text, $op, $append, $expected ) {
143 // assume NS_HELP defaults to wikitext
144 $name = "Help:ApiEditPageTest_testEditAppend_$count";
146 // -- create page (or not) -----------------------------------------
147 if ( $text !== null ) {
148 list( $re ) = $this->doApiRequestWithToken( [
151 'text' => $text, ] );
153 $this->assertSame( 'Success', $re['edit']['result'] ); // sanity
156 // -- try append/prepend --------------------------------------------
157 list( $re ) = $this->doApiRequestWithToken( [
160 $op . 'text' => $append, ] );
162 $this->assertSame( 'Success', $re['edit']['result'] );
164 // -- validate -----------------------------------------------------
165 $page = new WikiPage( Title
::newFromText( $name ) );
166 $content = $page->getContent();
167 $this->assertNotNull( $content, 'Page should have been created' );
169 $text = $content->getNativeData();
171 $this->assertSame( $expected, $text );
175 * Test editing of sections
177 public function testEditSection() {
178 $name = 'Help:ApiEditPageTest_testEditSection';
179 $page = WikiPage
::factory( Title
::newFromText( $name ) );
180 $text = "==section 1==\ncontent 1\n==section 2==\ncontent2";
181 // Preload the page with some text
182 $page->doEditContent( ContentHandler
::makeContent( $text, $page->getTitle() ), 'summary' );
184 list( $re ) = $this->doApiRequestWithToken( [
188 'text' => "==section 1==\nnew content 1",
190 $this->assertSame( 'Success', $re['edit']['result'] );
191 $newtext = WikiPage
::factory( Title
::newFromText( $name ) )
192 ->getContent( Revision
::RAW
)
194 $this->assertSame( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext );
196 // Test that we raise a 'nosuchsection' error
198 $this->doApiRequestWithToken( [
204 $this->fail( "Should have raised an ApiUsageException" );
205 } catch ( ApiUsageException
$e ) {
206 $this->assertTrue( self
::apiExceptionHasCode( $e, 'nosuchsection' ) );
211 * Test action=edit§ion=new
212 * Run it twice so we test adding a new section on a
213 * page that doesn't exist (T54830) and one that
216 public function testEditNewSection() {
217 $name = 'Help:ApiEditPageTest_testEditNewSection';
219 // Test on a page that does not already exist
220 $this->assertFalse( Title
::newFromText( $name )->exists() );
221 list( $re ) = $this->doApiRequestWithToken( [
226 'summary' => 'header',
229 $this->assertSame( 'Success', $re['edit']['result'] );
230 // Check the page text is correct
231 $text = WikiPage
::factory( Title
::newFromText( $name ) )
232 ->getContent( Revision
::RAW
)
234 $this->assertSame( "== header ==\n\ntest", $text );
236 // Now on one that does
237 $this->assertTrue( Title
::newFromText( $name )->exists() );
238 list( $re2 ) = $this->doApiRequestWithToken( [
243 'summary' => 'header',
246 $this->assertSame( 'Success', $re2['edit']['result'] );
247 $text = WikiPage
::factory( Title
::newFromText( $name ) )
248 ->getContent( Revision
::RAW
)
250 $this->assertSame( "== header ==\n\ntest\n\n== header ==\n\ntest", $text );
254 * Ensure we can edit through a redirect, if adding a section
256 public function testEdit_redirect() {
260 // assume NS_HELP defaults to wikitext
261 $name = "Help:ApiEditPageTest_testEdit_redirect_$count";
262 $title = Title
::newFromText( $name );
263 $page = WikiPage
::factory( $title );
265 $rname = "Help:ApiEditPageTest_testEdit_redirect_r$count";
266 $rtitle = Title
::newFromText( $rname );
267 $rpage = WikiPage
::factory( $rtitle );
269 // base edit for content
270 $page->doEditContent( new WikitextContent( "Foo" ),
271 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
272 $this->forceRevisionDate( $page, '20120101000000' );
273 $baseTime = $page->getRevision()->getTimestamp();
275 // base edit for redirect
276 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
277 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
278 $this->forceRevisionDate( $rpage, '20120101000000' );
280 // conflicting edit to redirect
281 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
282 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
283 $this->forceRevisionDate( $rpage, '20120101020202' );
285 // try to save edit, following the redirect
286 list( $re, , ) = $this->doApiRequestWithToken( [
289 'text' => 'nix bar!',
290 'basetimestamp' => $baseTime,
295 $this->assertSame( 'Success', $re['edit']['result'],
296 "no problems expected when following redirect" );
300 * Ensure we cannot edit through a redirect, if attempting to overwrite content
302 public function testEdit_redirectText() {
306 // assume NS_HELP defaults to wikitext
307 $name = "Help:ApiEditPageTest_testEdit_redirectText_$count";
308 $title = Title
::newFromText( $name );
309 $page = WikiPage
::factory( $title );
311 $rname = "Help:ApiEditPageTest_testEdit_redirectText_r$count";
312 $rtitle = Title
::newFromText( $rname );
313 $rpage = WikiPage
::factory( $rtitle );
315 // base edit for content
316 $page->doEditContent( new WikitextContent( "Foo" ),
317 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
318 $this->forceRevisionDate( $page, '20120101000000' );
319 $baseTime = $page->getRevision()->getTimestamp();
321 // base edit for redirect
322 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
323 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
324 $this->forceRevisionDate( $rpage, '20120101000000' );
326 // conflicting edit to redirect
327 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
328 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
329 $this->forceRevisionDate( $rpage, '20120101020202' );
331 // try to save edit, following the redirect but without creating a section
333 $this->doApiRequestWithToken( [
336 'text' => 'nix bar!',
337 'basetimestamp' => $baseTime,
341 $this->fail( 'redirect-appendonly error expected' );
342 } catch ( ApiUsageException
$ex ) {
343 $this->assertTrue( self
::apiExceptionHasCode( $ex, 'redirect-appendonly' ) );
347 public function testEditConflict() {
351 // assume NS_HELP defaults to wikitext
352 $name = "Help:ApiEditPageTest_testEditConflict_$count";
353 $title = Title
::newFromText( $name );
355 $page = WikiPage
::factory( $title );
358 $page->doEditContent( new WikitextContent( "Foo" ),
359 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
360 $this->forceRevisionDate( $page, '20120101000000' );
361 $baseTime = $page->getRevision()->getTimestamp();
364 $page->doEditContent( new WikitextContent( "Foo bar" ),
365 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
366 $this->forceRevisionDate( $page, '20120101020202' );
368 // try to save edit, expect conflict
370 $this->doApiRequestWithToken( [
373 'text' => 'nix bar!',
374 'basetimestamp' => $baseTime,
377 $this->fail( 'edit conflict expected' );
378 } catch ( ApiUsageException
$ex ) {
379 $this->assertTrue( self
::apiExceptionHasCode( $ex, 'editconflict' ) );
384 * Ensure that editing using section=new will prevent simple conflicts
386 public function testEditConflict_newSection() {
390 // assume NS_HELP defaults to wikitext
391 $name = "Help:ApiEditPageTest_testEditConflict_newSection_$count";
392 $title = Title
::newFromText( $name );
394 $page = WikiPage
::factory( $title );
397 $page->doEditContent( new WikitextContent( "Foo" ),
398 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
399 $this->forceRevisionDate( $page, '20120101000000' );
400 $baseTime = $page->getRevision()->getTimestamp();
403 $page->doEditContent( new WikitextContent( "Foo bar" ),
404 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
405 $this->forceRevisionDate( $page, '20120101020202' );
407 // try to save edit, expect no conflict
408 list( $re, , ) = $this->doApiRequestWithToken( [
411 'text' => 'nix bar!',
412 'basetimestamp' => $baseTime,
416 $this->assertSame( 'Success', $re['edit']['result'],
417 "no edit conflict expected here" );
420 public function testEditConflict_bug41990() {
425 * T43990: if the target page has a newer revision than the redirect, then editing the
426 * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously
427 * caused an edit conflict to be detected.
430 // assume NS_HELP defaults to wikitext
431 $name = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_$count";
432 $title = Title
::newFromText( $name );
433 $page = WikiPage
::factory( $title );
435 $rname = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_r$count";
436 $rtitle = Title
::newFromText( $rname );
437 $rpage = WikiPage
::factory( $rtitle );
439 // base edit for content
440 $page->doEditContent( new WikitextContent( "Foo" ),
441 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
442 $this->forceRevisionDate( $page, '20120101000000' );
444 // base edit for redirect
445 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
446 "testing 1", EDIT_NEW
, false, self
::$users['sysop']->getUser() );
447 $this->forceRevisionDate( $rpage, '20120101000000' );
449 // new edit to content
450 $page->doEditContent( new WikitextContent( "Foo bar" ),
451 "testing 2", EDIT_UPDATE
, $page->getLatest(), self
::$users['uploader']->getUser() );
452 $this->forceRevisionDate( $rpage, '20120101020202' );
454 // try to save edit; should work, following the redirect.
455 list( $re, , ) = $this->doApiRequestWithToken( [
458 'text' => 'nix bar!',
463 $this->assertSame( 'Success', $re['edit']['result'],
464 "no edit conflict expected here" );
468 * @param WikiPage $page
469 * @param string|int $timestamp
471 protected function forceRevisionDate( WikiPage
$page, $timestamp ) {
472 $dbw = wfGetDB( DB_MASTER
);
474 $dbw->update( 'revision',
475 [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
476 [ 'rev_id' => $page->getLatest() ] );
481 public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
482 $this->setExpectedException(
483 ApiUsageException
::class,
484 'Direct editing via API is not supported for content model ' .
485 'testing used by Dummy:ApiEditPageTest_nonTextPageEdit'
488 $this->doApiRequestWithToken( [
490 'title' => 'Dummy:ApiEditPageTest_nonTextPageEdit',
491 'text' => '{"animals":["kittens!"]}'
495 public function testSupportsDirectApiEditing_withContentHandlerOverride() {
496 $name = 'DummyNonText:ApiEditPageTest_testNonTextEdit';
497 $data = serialize( 'some bla bla text' );
499 $result = $this->doApiRequestWithToken( [
505 $apiResult = $result[0];
507 // Validate API result data
508 $this->assertArrayHasKey( 'edit', $apiResult );
509 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
510 $this->assertSame( 'Success', $apiResult['edit']['result'] );
512 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
513 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
515 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
517 // validate resulting revision
518 $page = WikiPage
::factory( Title
::newFromText( $name ) );
519 $this->assertSame( "testing-nontext", $page->getContentModel() );
520 $this->assertSame( $data, $page->getContent()->serialize() );
524 * This test verifies that after changing the content model
525 * of a page, undoing that edit via the API will also
526 * undo the content model change.
528 public function testUndoAfterContentModelChange() {
529 $name = 'Help:' . __FUNCTION__
;
530 $uploader = self
::$users['uploader']->getUser();
531 $sysop = self
::$users['sysop']->getUser();
533 $apiResult = $this->doApiRequestWithToken( [
536 'text' => 'some text',
537 ], null, $sysop )[0];
540 $this->assertArrayHasKey( 'edit', $apiResult );
541 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
542 $this->assertSame( 'Success', $apiResult['edit']['result'] );
543 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
544 // Content model is wikitext
545 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
547 // Convert the page to JSON
548 $apiResult = $this->doApiRequestWithToken( [
552 'contentmodel' => 'json',
553 ], null, $uploader )[0];
556 $this->assertArrayHasKey( 'edit', $apiResult );
557 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
558 $this->assertSame( 'Success', $apiResult['edit']['result'] );
559 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
560 $this->assertSame( 'json', $apiResult['edit']['contentmodel'] );
562 $apiResult = $this->doApiRequestWithToken( [
565 'undo' => $apiResult['edit']['newrevid']
566 ], null, $sysop )[0];
569 $this->assertArrayHasKey( 'edit', $apiResult );
570 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
571 $this->assertSame( 'Success', $apiResult['edit']['result'] );
572 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
573 // Check that the contentmodel is back to wikitext now.
574 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
577 // The tests below are mostly not commented because they do exactly what
578 // you'd expect from the name.
580 public function testCorrectContentFormat() {
581 $name = 'Help:' . ucfirst( __FUNCTION__
);
583 $this->doApiRequestWithToken( [
586 'text' => 'some text',
587 'contentmodel' => 'wikitext',
588 'contentformat' => 'text/x-wiki',
591 $this->assertTrue( Title
::newFromText( $name )->exists() );
594 public function testUnsupportedContentFormat() {
595 $name = 'Help:' . ucfirst( __FUNCTION__
);
597 $this->setExpectedException( ApiUsageException
::class,
598 'Unrecognized value for parameter "contentformat": nonexistent format.' );
601 $this->doApiRequestWithToken( [
604 'text' => 'some text',
605 'contentformat' => 'nonexistent format',
608 $this->assertFalse( Title
::newFromText( $name )->exists() );
612 public function testMismatchedContentFormat() {
613 $name = 'Help:' . ucfirst( __FUNCTION__
);
615 $this->setExpectedException( ApiUsageException
::class,
616 'The requested format text/plain is not supported for content ' .
617 "model wikitext used by $name." );
620 $this->doApiRequestWithToken( [
623 'text' => 'some text',
624 'contentmodel' => 'wikitext',
625 'contentformat' => 'text/plain',
628 $this->assertFalse( Title
::newFromText( $name )->exists() );
632 public function testUndoToInvalidRev() {
633 $name = 'Help:' . ucfirst( __FUNCTION__
);
635 $revId = $this->editPage( $name, 'Some text' )->value
['revision']
639 $this->setExpectedException( ApiUsageException
::class,
640 "There is no revision with ID $revId." );
642 $this->doApiRequestWithToken( [
650 * Tests what happens if the undo parameter is a valid revision, but
651 * the undoafter parameter doesn't refer to a revision that exists in the
654 public function testUndoAfterToInvalidRev() {
655 // We can't just pick a large number for undoafter (as in
656 // testUndoToInvalidRev above), because then MediaWiki will helpfully
657 // assume we switched around undo and undoafter and we'll test the code
658 // path for undo being invalid, not undoafter. So instead we delete
659 // the revision from the database. In real life this case could come
660 // up if a revision number was skipped, e.g., if two transactions try
661 // to insert new revision rows at once and the first one to succeed
663 $name = 'Help:' . ucfirst( __FUNCTION__
);
664 $titleObj = Title
::newFromText( $name );
666 $revId1 = $this->editPage( $name, '1' )->value
['revision']->getId();
667 $revId2 = $this->editPage( $name, '2' )->value
['revision']->getId();
668 $revId3 = $this->editPage( $name, '3' )->value
['revision']->getId();
670 // Make the middle revision disappear
671 $dbw = wfGetDB( DB_MASTER
);
672 $dbw->delete( 'revision', [ 'rev_id' => $revId2 ], __METHOD__
);
673 $dbw->update( 'revision', [ 'rev_parent_id' => $revId1 ],
674 [ 'rev_id' => $revId3 ], __METHOD__
);
676 $this->setExpectedException( ApiUsageException
::class,
677 "There is no revision with ID $revId2." );
679 $this->doApiRequestWithToken( [
683 'undoafter' => $revId2,
688 * Tests what happens if the undo parameter is a valid revision, but
689 * undoafter is hidden (rev_deleted).
691 public function testUndoAfterToHiddenRev() {
692 $name = 'Help:' . ucfirst( __FUNCTION__
);
693 $titleObj = Title
::newFromText( $name );
695 $this->editPage( $name, '0' );
697 $revId1 = $this->editPage( $name, '1' )->value
['revision']->getId();
699 $revId2 = $this->editPage( $name, '2' )->value
['revision']->getId();
701 // Hide the middle revision
702 $list = RevisionDeleter
::createList( 'revision',
703 RequestContext
::getMain(), $titleObj, [ $revId1 ] );
704 $list->setVisibility( [
705 'value' => [ Revision
::DELETED_TEXT
=> 1 ],
706 'comment' => 'Bye-bye',
709 $this->setExpectedException( ApiUsageException
::class,
710 "There is no revision with ID $revId1." );
712 $this->doApiRequestWithToken( [
716 'undoafter' => $revId1,
721 * Test undo when a revision with a higher id has an earlier timestamp.
722 * This can happen if importing an old revision.
724 public function testUndoWithSwappedRevisions() {
725 $name = 'Help:' . ucfirst( __FUNCTION__
);
726 $titleObj = Title
::newFromText( $name );
728 $this->editPage( $name, '0' );
730 $revId2 = $this->editPage( $name, '2' )->value
['revision']->getId();
732 $revId1 = $this->editPage( $name, '1' )->value
['revision']->getId();
734 // Now monkey with the timestamp
735 $dbw = wfGetDB( DB_MASTER
);
738 [ 'rev_timestamp' => wfTimestamp( TS_MW
, time() - 86400 ) ],
739 [ 'rev_id' => $revId1 ],
743 $this->doApiRequestWithToken( [
747 'undoafter' => $revId1,
750 $text = ( new WikiPage( $titleObj ) )->getContent()->getNativeData();
752 // This is wrong! It should be 1. But let's test for our incorrect
753 // behavior for now, so if someone fixes it they'll fix the test as
754 // well to expect 1. If we disabled the test, it might stay disabled
755 // even once the bug is fixed, which would be a shame.
756 $this->assertSame( '2', $text );
759 public function testUndoWithConflicts() {
760 $name = 'Help:' . ucfirst( __FUNCTION__
);
762 $this->setExpectedException( ApiUsageException
::class,
763 'The edit could not be undone due to conflicting intermediate edits.' );
765 $this->editPage( $name, '1' );
767 $revId = $this->editPage( $name, '2' )->value
['revision']->getId();
769 $this->editPage( $name, '3' );
771 $this->doApiRequestWithToken( [
777 $text = ( new WikiPage( Title
::newFromText( $name ) ) )->getContent()
779 $this->assertSame( '3', $text );
783 * undoafter is supposed to be less than undo. If not, we reverse their
784 * meaning, so that the two are effectively interchangeable.
786 public function testReversedUndoAfter() {
787 $name = 'Help:' . ucfirst( __FUNCTION__
);
789 $this->editPage( $name, '0' );
790 $revId1 = $this->editPage( $name, '1' )->value
['revision']->getId();
791 $revId2 = $this->editPage( $name, '2' )->value
['revision']->getId();
793 $this->doApiRequestWithToken( [
797 'undoafter' => $revId2,
800 $text = ( new WikiPage( Title
::newFromText( $name ) ) )->getContent()
802 $this->assertSame( '1', $text );
805 public function testUndoToRevFromDifferentPage() {
806 $name = 'Help:' . ucfirst( __FUNCTION__
);
808 $this->editPage( "$name-1", 'Some text' );
809 $revId = $this->editPage( "$name-1", 'Some more text' )
810 ->value
['revision']->getId();
812 $this->editPage( "$name-2", 'Some text' );
814 $this->setExpectedException( ApiUsageException
::class,
815 "r$revId is not a revision of $name-2." );
817 $this->doApiRequestWithToken( [
819 'title' => "$name-2",
824 public function testUndoAfterToRevFromDifferentPage() {
825 $name = 'Help:' . ucfirst( __FUNCTION__
);
827 $revId1 = $this->editPage( "$name-1", 'Some text' )
828 ->value
['revision']->getId();
830 $revId2 = $this->editPage( "$name-2", 'Some text' )
831 ->value
['revision']->getId();
833 $this->setExpectedException( ApiUsageException
::class,
834 "r$revId1 is not a revision of $name-2." );
836 $this->doApiRequestWithToken( [
838 'title' => "$name-2",
840 'undoafter' => $revId1,
844 public function testMd5Text() {
845 $name = 'Help:' . ucfirst( __FUNCTION__
);
847 $this->assertFalse( Title
::newFromText( $name )->exists() );
849 $this->doApiRequestWithToken( [
852 'text' => 'Some text',
853 'md5' => md5( 'Some text' ),
856 $this->assertTrue( Title
::newFromText( $name )->exists() );
859 public function testMd5PrependText() {
860 $name = 'Help:' . ucfirst( __FUNCTION__
);
862 $this->editPage( $name, 'Some text' );
864 $this->doApiRequestWithToken( [
867 'prependtext' => 'Alert: ',
868 'md5' => md5( 'Alert: ' ),
871 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
872 ->getContent()->getNativeData();
873 $this->assertSame( 'Alert: Some text', $text );
876 public function testMd5AppendText() {
877 $name = 'Help:' . ucfirst( __FUNCTION__
);
879 $this->editPage( $name, 'Some text' );
881 $this->doApiRequestWithToken( [
884 'appendtext' => ' is nice',
885 'md5' => md5( ' is nice' ),
888 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
889 ->getContent()->getNativeData();
890 $this->assertSame( 'Some text is nice', $text );
893 public function testMd5PrependAndAppendText() {
894 $name = 'Help:' . ucfirst( __FUNCTION__
);
896 $this->editPage( $name, 'Some text' );
898 $this->doApiRequestWithToken( [
901 'prependtext' => 'Alert: ',
902 'appendtext' => ' is nice',
903 'md5' => md5( 'Alert: is nice' ),
906 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
907 ->getContent()->getNativeData();
908 $this->assertSame( 'Alert: Some text is nice', $text );
911 public function testIncorrectMd5Text() {
912 $name = 'Help:' . ucfirst( __FUNCTION__
);
914 $this->setExpectedException( ApiUsageException
::class,
915 'The supplied MD5 hash was incorrect.' );
917 $this->doApiRequestWithToken( [
920 'text' => 'Some text',
925 public function testIncorrectMd5PrependText() {
926 $name = 'Help:' . ucfirst( __FUNCTION__
);
928 $this->setExpectedException( ApiUsageException
::class,
929 'The supplied MD5 hash was incorrect.' );
931 $this->doApiRequestWithToken( [
934 'prependtext' => 'Some ',
935 'appendtext' => 'text',
936 'md5' => md5( 'Some ' ),
940 public function testIncorrectMd5AppendText() {
941 $name = 'Help:' . ucfirst( __FUNCTION__
);
943 $this->setExpectedException( ApiUsageException
::class,
944 'The supplied MD5 hash was incorrect.' );
946 $this->doApiRequestWithToken( [
949 'prependtext' => 'Some ',
950 'appendtext' => 'text',
951 'md5' => md5( 'text' ),
955 public function testCreateOnly() {
956 $name = 'Help:' . ucfirst( __FUNCTION__
);
958 $this->setExpectedException( ApiUsageException
::class,
959 'The article you tried to create has been created already.' );
961 $this->editPage( $name, 'Some text' );
962 $this->assertTrue( Title
::newFromText( $name )->exists() );
965 $this->doApiRequestWithToken( [
968 'text' => 'Some more text',
972 // Validate that content was not changed
973 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
974 ->getContent()->getNativeData();
976 $this->assertSame( 'Some text', $text );
980 public function testNoCreate() {
981 $name = 'Help:' . ucfirst( __FUNCTION__
);
983 $this->setExpectedException( ApiUsageException
::class,
984 "The page you specified doesn't exist." );
986 $this->assertFalse( Title
::newFromText( $name )->exists() );
989 $this->doApiRequestWithToken( [
992 'text' => 'Some text',
996 $this->assertFalse( Title
::newFromText( $name )->exists() );
1001 * Appending/prepending is currently only supported for TextContent. We
1002 * test this right now, and when support is added this test should be
1003 * replaced by tests that the support is correct.
1005 public function testAppendWithNonTextContentHandler() {
1006 $name = 'MediaWiki:' . ucfirst( __FUNCTION__
);
1008 $this->setExpectedException( ApiUsageException
::class,
1009 "Can't append to pages using content model testing-nontext." );
1011 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1012 function ( Title
$title, &$model ) use ( $name ) {
1013 if ( $title->getPrefixedText() === $name ) {
1014 $model = 'testing-nontext';
1020 $this->doApiRequestWithToken( [
1023 'appendtext' => 'Some text',
1027 public function testAppendInMediaWikiNamespace() {
1028 $name = 'MediaWiki:' . ucfirst( __FUNCTION__
);
1030 $this->assertFalse( Title
::newFromText( $name )->exists() );
1032 $this->doApiRequestWithToken( [
1035 'appendtext' => 'Some text',
1038 $this->assertTrue( Title
::newFromText( $name )->exists() );
1041 public function testAppendInMediaWikiNamespaceWithSerializationError() {
1042 $name = 'MediaWiki:' . ucfirst( __FUNCTION__
);
1044 $this->setExpectedException( ApiUsageException
::class,
1045 'Content serialization failed: Could not unserialize content' );
1047 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1048 function ( Title
$title, &$model ) use ( $name ) {
1049 if ( $title->getPrefixedText() === $name ) {
1050 $model = 'testing-serialize-error';
1056 $this->doApiRequestWithToken( [
1059 'appendtext' => 'Some text',
1063 public function testAppendNewSection() {
1064 $name = 'Help:' . ucfirst( __FUNCTION__
);
1066 $this->editPage( $name, 'Initial content' );
1068 $this->doApiRequestWithToken( [
1071 'appendtext' => '== New section ==',
1075 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1076 ->getContent()->getNativeData();
1078 $this->assertSame( "Initial content\n\n== New section ==", $text );
1081 public function testAppendNewSectionWithInvalidContentModel() {
1082 $name = 'Help:' . ucfirst( __FUNCTION__
);
1084 $this->setExpectedException( ApiUsageException
::class,
1085 'Sections are not supported for content model text.' );
1087 $this->editPage( $name, 'Initial content' );
1089 $this->doApiRequestWithToken( [
1092 'appendtext' => '== New section ==',
1094 'contentmodel' => 'text',
1098 public function testAppendNewSectionWithTitle() {
1099 $name = 'Help:' . ucfirst( __FUNCTION__
);
1101 $this->editPage( $name, 'Initial content' );
1103 $this->doApiRequestWithToken( [
1106 'sectiontitle' => 'My section',
1107 'appendtext' => 'More content',
1111 $page = new WikiPage( Title
::newFromText( $name ) );
1113 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1114 $page->getContent()->getNativeData() );
1115 $this->assertSame( '/* My section */ new section',
1116 $page->getRevision()->getComment() );
1119 public function testAppendNewSectionWithSummary() {
1120 $name = 'Help:' . ucfirst( __FUNCTION__
);
1122 $this->editPage( $name, 'Initial content' );
1124 $this->doApiRequestWithToken( [
1127 'appendtext' => 'More content',
1129 'summary' => 'Add new section',
1132 $page = new WikiPage( Title
::newFromText( $name ) );
1134 $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content",
1135 $page->getContent()->getNativeData() );
1136 // EditPage actually assumes the summary is the section name here
1137 $this->assertSame( '/* Add new section */ new section',
1138 $page->getRevision()->getComment() );
1141 public function testAppendNewSectionWithTitleAndSummary() {
1142 $name = 'Help:' . ucfirst( __FUNCTION__
);
1144 $this->editPage( $name, 'Initial content' );
1146 $this->doApiRequestWithToken( [
1149 'sectiontitle' => 'My section',
1150 'appendtext' => 'More content',
1152 'summary' => 'Add new section',
1155 $page = new WikiPage( Title
::newFromText( $name ) );
1157 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1158 $page->getContent()->getNativeData() );
1159 $this->assertSame( 'Add new section',
1160 $page->getRevision()->getComment() );
1163 public function testAppendToSection() {
1164 $name = 'Help:' . ucfirst( __FUNCTION__
);
1166 $this->editPage( $name, "== Section 1 ==\n\nContent\n\n" .
1167 "== Section 2 ==\n\nFascinating!" );
1169 $this->doApiRequestWithToken( [
1172 'appendtext' => ' and more content',
1176 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1177 ->getContent()->getNativeData();
1179 $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" .
1180 "== Section 2 ==\n\nFascinating!", $text );
1183 public function testAppendToFirstSection() {
1184 $name = 'Help:' . ucfirst( __FUNCTION__
);
1186 $this->editPage( $name, "Content\n\n== Section 1 ==\n\nFascinating!" );
1188 $this->doApiRequestWithToken( [
1191 'appendtext' => ' and more content',
1195 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1196 ->getContent()->getNativeData();
1198 $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" .
1199 "Fascinating!", $text );
1202 public function testAppendToNonexistentSection() {
1203 $name = 'Help:' . ucfirst( __FUNCTION__
);
1205 $this->setExpectedException( ApiUsageException
::class, 'There is no section 1.' );
1207 $this->editPage( $name, 'Content' );
1210 $this->doApiRequestWithToken( [
1213 'appendtext' => ' and more content',
1217 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1218 ->getContent()->getNativeData();
1220 $this->assertSame( 'Content', $text );
1224 public function testEditMalformedSection() {
1225 $name = 'Help:' . ucfirst( __FUNCTION__
);
1227 $this->setExpectedException( ApiUsageException
::class,
1228 'The "section" parameter must be a valid section ID or "new".' );
1229 $this->editPage( $name, 'Content' );
1232 $this->doApiRequestWithToken( [
1235 'text' => 'Different content',
1236 'section' => 'It is unlikely that this is valid',
1239 $text = ( new WikiPage( Title
::newFromText( $name ) ) )
1240 ->getContent()->getNativeData();
1242 $this->assertSame( 'Content', $text );
1246 public function testEditWithStartTimestamp() {
1247 $name = 'Help:' . ucfirst( __FUNCTION__
);
1248 $this->setExpectedException( ApiUsageException
::class,
1249 'The page has been deleted since you fetched its timestamp.' );
1251 $startTime = MWTimestamp
::convert( TS_MW
, time() - 1 );
1253 $this->editPage( $name, 'Some text' );
1255 $pageObj = new WikiPage( Title
::newFromText( $name ) );
1256 $pageObj->doDeleteArticle( 'Bye-bye' );
1258 $this->assertFalse( $pageObj->exists() );
1261 $this->doApiRequestWithToken( [
1264 'text' => 'Different text',
1265 'starttimestamp' => $startTime,
1268 $this->assertFalse( $pageObj->exists() );
1272 public function testEditMinor() {
1273 $name = 'Help:' . ucfirst( __FUNCTION__
);
1275 $this->editPage( $name, 'Some text' );
1277 $this->doApiRequestWithToken( [
1280 'text' => 'Different text',
1284 $revisionStore = \MediaWiki\MediaWikiServices
::getInstance()->getRevisionStore();
1285 $revision = $revisionStore->getRevisionByTitle( Title
::newFromText( $name ) );
1286 $this->assertTrue( $revision->isMinor() );
1289 public function testEditRecreate() {
1290 $name = 'Help:' . ucfirst( __FUNCTION__
);
1292 $startTime = MWTimestamp
::convert( TS_MW
, time() - 1 );
1294 $this->editPage( $name, 'Some text' );
1296 $pageObj = new WikiPage( Title
::newFromText( $name ) );
1297 $pageObj->doDeleteArticle( 'Bye-bye' );
1299 $this->assertFalse( $pageObj->exists() );
1301 $this->doApiRequestWithToken( [
1304 'text' => 'Different text',
1305 'starttimestamp' => $startTime,
1309 $this->assertTrue( Title
::newFromText( $name )->exists() );
1312 public function testEditWatch() {
1313 $name = 'Help:' . ucfirst( __FUNCTION__
);
1314 $user = self
::$users['sysop']->getUser();
1316 $this->doApiRequestWithToken( [
1319 'text' => 'Some text',
1323 $this->assertTrue( Title
::newFromText( $name )->exists() );
1324 $this->assertTrue( $user->isWatched( Title
::newFromText( $name ) ) );
1327 public function testEditUnwatch() {
1328 $name = 'Help:' . ucfirst( __FUNCTION__
);
1329 $user = self
::$users['sysop']->getUser();
1330 $titleObj = Title
::newFromText( $name );
1332 $user->addWatch( $titleObj );
1334 $this->assertFalse( $titleObj->exists() );
1335 $this->assertTrue( $user->isWatched( $titleObj ) );
1337 $this->doApiRequestWithToken( [
1340 'text' => 'Some text',
1344 $this->assertTrue( $titleObj->exists() );
1345 $this->assertFalse( $user->isWatched( $titleObj ) );
1348 public function testEditWithTag() {
1349 $name = 'Help:' . ucfirst( __FUNCTION__
);
1351 ChangeTags
::defineTag( 'custom tag' );
1353 $revId = $this->doApiRequestWithToken( [
1356 'text' => 'Some text',
1357 'tags' => 'custom tag',
1358 ] )[0]['edit']['newrevid'];
1360 $dbw = wfGetDB( DB_MASTER
);
1361 $this->assertSame( 'custom tag', $dbw->selectField(
1362 'change_tag', 'ct_tag', [ 'ct_rev_id' => $revId ], __METHOD__
) );
1365 public function testEditWithoutTagPermission() {
1366 $name = 'Help:' . ucfirst( __FUNCTION__
);
1368 $this->setExpectedException( ApiUsageException
::class,
1369 'You do not have permission to apply change tags along with your changes.' );
1371 $this->assertFalse( Title
::newFromText( $name )->exists() );
1373 ChangeTags
::defineTag( 'custom tag' );
1374 $this->setMwGlobals( 'wgRevokePermissions',
1375 [ 'user' => [ 'applychangetags' => true ] ] );
1377 $this->doApiRequestWithToken( [
1380 'text' => 'Some text',
1381 'tags' => 'custom tag',
1384 $this->assertFalse( Title
::newFromText( $name )->exists() );
1388 public function testEditAbortedByHook() {
1389 $name = 'Help:' . ucfirst( __FUNCTION__
);
1391 $this->setExpectedException( ApiUsageException
::class,
1392 'The modification you tried to make was aborted by an extension.' );
1394 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1395 'hook-APIEditBeforeSave-closure)' );
1397 $this->setTemporaryHook( 'APIEditBeforeSave',
1404 $this->doApiRequestWithToken( [
1407 'text' => 'Some text',
1410 $this->assertFalse( Title
::newFromText( $name )->exists() );
1414 public function testEditAbortedByHookWithCustomOutput() {
1415 $name = 'Help:' . ucfirst( __FUNCTION__
);
1417 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1418 'hook-APIEditBeforeSave-closure)' );
1420 $this->setTemporaryHook( 'APIEditBeforeSave',
1421 function ( $unused1, $unused2, &$r ) {
1422 $r['msg'] = 'Some message';
1426 $result = $this->doApiRequestWithToken( [
1429 'text' => 'Some text',
1431 Wikimedia\restoreWarnings
();
1433 $this->assertSame( [ 'msg' => 'Some message', 'result' => 'Failure' ],
1434 $result[0]['edit'] );
1436 $this->assertFalse( Title
::newFromText( $name )->exists() );
1439 public function testEditAbortedByEditPageHookWithResult() {
1440 $name = 'Help:' . ucfirst( __FUNCTION__
);
1442 $this->setTemporaryHook( 'EditFilterMergedContent',
1443 function ( $unused1, $unused2, Status
$status ) {
1444 $status->apiHookResult
= [ 'msg' => 'A message for you!' ];
1448 $res = $this->doApiRequestWithToken( [
1451 'text' => 'Some text',
1454 $this->assertFalse( Title
::newFromText( $name )->exists() );
1455 $this->assertSame( [ 'edit' => [ 'msg' => 'A message for you!',
1456 'result' => 'Failure' ] ], $res[0] );
1459 public function testEditAbortedByEditPageHookWithNoResult() {
1460 $name = 'Help:' . ucfirst( __FUNCTION__
);
1462 $this->setExpectedException( ApiUsageException
::class,
1463 'The modification you tried to make was aborted by an extension.' );
1465 $this->setTemporaryHook( 'EditFilterMergedContent',
1472 $this->doApiRequestWithToken( [
1475 'text' => 'Some text',
1478 $this->assertFalse( Title
::newFromText( $name )->exists() );
1482 public function testEditWhileBlocked() {
1483 $name = 'Help:' . ucfirst( __FUNCTION__
);
1485 $this->setExpectedException( ApiUsageException
::class,
1486 'You have been blocked from editing.' );
1488 $block = new Block( [
1489 'address' => self
::$users['sysop']->getUser()->getName(),
1490 'by' => self
::$users['sysop']->getUser()->getId(),
1491 'reason' => 'Capriciousness',
1492 'timestamp' => '19370101000000',
1493 'expiry' => 'infinity',
1498 $this->doApiRequestWithToken( [
1501 'text' => 'Some text',
1505 self
::$users['sysop']->getUser()->clearInstanceCache();
1509 public function testEditWhileReadOnly() {
1510 $name = 'Help:' . ucfirst( __FUNCTION__
);
1512 $this->setExpectedException( ApiUsageException
::class,
1513 'The wiki is currently in read-only mode.' );
1515 $svc = \MediaWiki\MediaWikiServices
::getInstance()->getReadOnlyMode();
1516 $svc->setReason( "Read-only for testing" );
1519 $this->doApiRequestWithToken( [
1522 'text' => 'Some text',
1525 $svc->setReason( false );
1529 public function testCreateImageRedirectAnon() {
1530 $name = 'File:' . ucfirst( __FUNCTION__
);
1532 $this->setExpectedException( ApiUsageException
::class,
1533 "Anonymous users can't create image redirects." );
1535 $this->doApiRequestWithToken( [
1538 'text' => '#REDIRECT [[File:Other file.png]]',
1539 ], null, new User() );
1542 public function testCreateImageRedirectLoggedIn() {
1543 $name = 'File:' . ucfirst( __FUNCTION__
);
1545 $this->setExpectedException( ApiUsageException
::class,
1546 "You don't have permission to create image redirects." );
1548 $this->setMwGlobals( 'wgRevokePermissions',
1549 [ 'user' => [ 'upload' => true ] ] );
1551 $this->doApiRequestWithToken( [
1554 'text' => '#REDIRECT [[File:Other file.png]]',
1558 public function testTooBigEdit() {
1559 $name = 'Help:' . ucfirst( __FUNCTION__
);
1561 $this->setExpectedException( ApiUsageException
::class,
1562 'The content you supplied exceeds the article size limit of 1 kilobyte.' );
1564 $this->setMwGlobals( 'wgMaxArticleSize', 1 );
1566 $text = str_repeat( '!', 1025 );
1568 $this->doApiRequestWithToken( [
1575 public function testProhibitedAnonymousEdit() {
1576 $name = 'Help:' . ucfirst( __FUNCTION__
);
1578 $this->setExpectedException( ApiUsageException
::class,
1579 'The action you have requested is limited to users in the group: ' );
1581 $this->setMwGlobals( 'wgRevokePermissions', [ '*' => [ 'edit' => true ] ] );
1583 $this->doApiRequestWithToken( [
1586 'text' => 'Some text',
1587 ], null, new User() );
1590 public function testProhibitedChangeContentModel() {
1591 $name = 'Help:' . ucfirst( __FUNCTION__
);
1593 $this->setExpectedException( ApiUsageException
::class,
1594 "You don't have permission to change the content model of a page." );
1596 $this->setMwGlobals( 'wgRevokePermissions',
1597 [ 'user' => [ 'editcontentmodel' => true ] ] );
1599 $this->doApiRequestWithToken( [
1602 'text' => 'Some text',
1603 'contentmodel' => 'json',