Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / includes / api / ApiEditPageTest.php
blob2cdbd8f18068f400d449059998968ff7e4ef4d5c
1 <?php
3 namespace MediaWiki\Tests\Api;
5 use MediaWiki\Api\ApiUsageException;
6 use MediaWiki\Block\DatabaseBlock;
7 use MediaWiki\CommentStore\CommentStoreComment;
8 use MediaWiki\Content\JavaScriptContent;
9 use MediaWiki\Content\WikitextContent;
10 use MediaWiki\Context\RequestContext;
11 use MediaWiki\MainConfigNames;
12 use MediaWiki\Revision\RevisionRecord;
13 use MediaWiki\Status\Status;
14 use MediaWiki\Tests\User\TempUser\TempUserTestTrait;
15 use MediaWiki\Title\Title;
16 use MediaWiki\Title\TitleValue;
17 use MediaWiki\User\User;
18 use MediaWiki\Utils\MWTimestamp;
19 use RevisionDeleter;
20 use Wikimedia\Rdbms\IDBAccessObject;
21 use WikiPage;
23 /**
24 * Tests for MediaWiki api.php?action=edit.
26 * @author Daniel Kinzler
28 * @group API
29 * @group Database
30 * @group medium
32 * @covers \MediaWiki\Api\ApiEditPage
34 class ApiEditPageTest extends ApiTestCase {
36 use TempUserTestTrait;
38 protected function setUp(): void {
39 parent::setUp();
41 $this->overrideConfigValues( [
42 MainConfigNames::ExtraNamespaces => [
43 12312 => 'Dummy',
44 12313 => 'Dummy_talk',
45 12314 => 'DummyNonText',
46 12315 => 'DummyNonText_talk',
48 MainConfigNames::NamespaceContentModels => [
49 12312 => 'testing',
50 12314 => 'testing-nontext',
52 MainConfigNames::WatchlistExpiry => true,
53 MainConfigNames::WatchlistExpiryMaxDuration => '6 months',
54 ] );
55 $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
56 'testing' => 'DummyContentHandlerForTesting',
57 'testing-nontext' => 'DummyNonTextContentHandler',
58 'testing-serialize-error' => 'DummySerializeErrorContentHandler',
59 ] );
62 public function testEdit() {
63 $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext
65 // -- test new page --------------------------------------------
66 $apiResult = $this->doApiRequestWithToken( [
67 'action' => 'edit',
68 'title' => $name,
69 'text' => 'some text',
70 ] );
71 $apiResult = $apiResult[0];
73 // Validate API result data
74 $this->assertArrayHasKey( 'edit', $apiResult );
75 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
76 $this->assertSame( 'Success', $apiResult['edit']['result'] );
78 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
79 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
81 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
83 // -- test existing page, no change ----------------------------
84 $data = $this->doApiRequestWithToken( [
85 'action' => 'edit',
86 'title' => $name,
87 'text' => 'some text',
88 ] );
90 $this->assertSame( 'Success', $data[0]['edit']['result'] );
92 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
93 $this->assertArrayHasKey( 'nochange', $data[0]['edit'] );
95 // -- test existing page, with change --------------------------
96 $data = $this->doApiRequestWithToken( [
97 'action' => 'edit',
98 'title' => $name,
99 'text' => 'different text'
100 ] );
102 $this->assertSame( 'Success', $data[0]['edit']['result'] );
104 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
105 $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] );
107 $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] );
108 $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] );
109 $this->assertNotEquals(
110 $data[0]['edit']['newrevid'],
111 $data[0]['edit']['oldrevid'],
112 "revision id should change after edit"
117 * @return array
119 public static function provideEditAppend() {
120 return [
121 [ # 0: append
122 'foo', 'append', 'bar', "foobar"
124 [ # 1: prepend
125 'foo', 'prepend', 'bar', "barfoo"
127 [ # 2: append to empty page
128 '', 'append', 'foo', "foo"
130 [ # 3: prepend to empty page
131 '', 'prepend', 'foo', "foo"
133 [ # 4: append to non-existing page
134 null, 'append', 'foo', "foo"
136 [ # 5: prepend to non-existing page
137 null, 'prepend', 'foo', "foo"
143 * @dataProvider provideEditAppend
145 public function testEditAppend( $text, $op, $append, $expected ) {
146 static $count = 0;
147 $count++;
149 // assume NS_HELP defaults to wikitext
150 $title = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEditAppend_$count" );
152 // -- create page (or not) -----------------------------------------
153 if ( $text !== null ) {
154 [ $re ] = $this->doApiRequestWithToken( [
155 'action' => 'edit',
156 'title' => $title->getPrefixedText(),
157 'text' => $text, ] );
159 $this->assertSame( 'Success', $re['edit']['result'] );
162 // -- try append/prepend --------------------------------------------
163 [ $re ] = $this->doApiRequestWithToken( [
164 'action' => 'edit',
165 'title' => $title->getPrefixedText(),
166 $op . 'text' => $append, ] );
168 $this->assertSame( 'Success', $re['edit']['result'] );
170 // -- validate -----------------------------------------------------
171 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
172 $content = $page->getContent();
173 $this->assertNotNull( $content, 'Page should have been created' );
175 $text = $content->getText();
177 $this->assertSame( $expected, $text );
181 * Test editing of sections
183 public function testEditSection() {
184 $title = Title::makeTitle( NS_HELP, 'ApiEditPageTest_testEditSection' );
185 $wikiPageFactory = $this->getServiceContainer()->getWikiPageFactory();
186 $page = $wikiPageFactory->newFromTitle( $title );
187 $text = "==section 1==\ncontent 1\n==section 2==\ncontent2";
188 // Preload the page with some text
189 $page->doUserEditContent(
190 $page->getContentHandler()->unserializeContent( $text ),
191 $this->getTestSysop()->getAuthority(),
192 'summary'
195 [ $re ] = $this->doApiRequestWithToken( [
196 'action' => 'edit',
197 'title' => $title->getPrefixedText(),
198 'section' => '1',
199 'text' => "==section 1==\nnew content 1",
200 ] );
201 $this->assertSame( 'Success', $re['edit']['result'] );
202 $newtext = $wikiPageFactory->newFromTitle( $title )
203 ->getContent( RevisionRecord::RAW )
204 ->getText();
205 $this->assertSame( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext );
207 // Test that we raise a 'nosuchsection' error
208 try {
209 $this->doApiRequestWithToken( [
210 'action' => 'edit',
211 'title' => $title->getPrefixedText(),
212 'section' => '9999',
213 'text' => 'text',
214 ] );
215 $this->fail( "Should have raised an ApiUsageException" );
216 } catch ( ApiUsageException $e ) {
217 $this->assertApiErrorCode( 'nosuchsection', $e );
222 * Test action=edit&section=new
223 * Run it twice so we test adding a new section on a
224 * page that doesn't exist (T54830) and one that
225 * does exist
227 public function testEditNewSection() {
228 $title = Title::makeTitle( NS_HELP, 'ApiEditPageTest_testEditNewSection' );
229 $wikiPageFactory = $this->getServiceContainer()->getWikiPageFactory();
231 // Test on a page that does not already exist
232 $this->assertFalse( $title->exists() );
233 [ $re ] = $this->doApiRequestWithToken( [
234 'action' => 'edit',
235 'title' => $title->getPrefixedText(),
236 'section' => 'new',
237 'text' => 'test',
238 'summary' => 'header',
239 ] );
241 $this->assertSame( 'Success', $re['edit']['result'] );
242 // Check the page text is correct
243 $text = $wikiPageFactory->newFromTitle( $title )
244 ->getContent( RevisionRecord::RAW )
245 ->getText();
246 $this->assertSame( "== header ==\n\ntest", $text );
248 // Now on one that does
249 $this->assertTrue( $title->exists( IDBAccessObject::READ_LATEST ) );
250 [ $re2 ] = $this->doApiRequestWithToken( [
251 'action' => 'edit',
252 'title' => $title->getPrefixedText(),
253 'section' => 'new',
254 'text' => 'test',
255 'summary' => 'header',
256 ] );
258 $this->assertSame( 'Success', $re2['edit']['result'] );
259 $text = $wikiPageFactory->newFromTitle( $title )
260 ->getContent( RevisionRecord::RAW )
261 ->getText();
262 $this->assertSame( "== header ==\n\ntest\n\n== header ==\n\ntest", $text );
266 * Test action=edit&section=new with different combinations of summary and sectiontitle.
268 * @dataProvider provideEditNewSectionSummarySectiontitle
270 public function testEditNewSectionSummarySectiontitle(
271 $sectiontitle,
272 $summary,
273 $expectedText,
274 $expectedSummary
276 static $count = 0;
277 $count++;
278 $title = Title::makeTitle( NS_HELP, 'ApiEditPageTest_testEditNewSectionSummarySectiontitle' . $count );
280 // Test edit 1 (new page)
281 $this->doApiRequestWithToken( [
282 'action' => 'edit',
283 'title' => $title->getPrefixedText(),
284 'section' => 'new',
285 'text' => 'text',
286 'sectiontitle' => $sectiontitle,
287 'summary' => $summary,
288 ] );
290 $wikiPageFactory = $this->getServiceContainer()->getWikiPageFactory();
291 $wikiPage = $wikiPageFactory->newFromTitle( $title );
293 // Check the page text is correct
294 $savedText = $wikiPage->getContent( RevisionRecord::RAW )->getText();
295 $this->assertSame( $expectedText, $savedText, 'Correct text saved (new page)' );
297 // Check that the edit summary is correct
298 // (when not provided or empty, there is an autogenerated summary for page creation)
299 $savedSummary = $wikiPage->getRevisionRecord()->getComment( RevisionRecord::RAW )->text;
300 $expectedSummaryNew = $expectedSummary ?: wfMessage( 'autosumm-new' )->rawParams( $expectedText )
301 ->inContentLanguage()->text();
302 $this->assertSame( $expectedSummaryNew, $savedSummary, 'Correct summary saved (new page)' );
304 // Clear the page
305 $this->editPage( $wikiPage, '' );
307 // Test edit 2 (existing page)
308 $this->doApiRequestWithToken( [
309 'action' => 'edit',
310 'title' => $title->getPrefixedText(),
311 'section' => 'new',
312 'text' => 'text',
313 'sectiontitle' => $sectiontitle,
314 'summary' => $summary,
315 ] );
317 $wikiPage = $wikiPageFactory->newFromTitle( $title );
319 // Check the page text is correct
320 $savedText = $wikiPage->getContent( RevisionRecord::RAW )->getText();
321 $this->assertSame( $expectedText, $savedText, 'Correct text saved (existing page)' );
323 // Check that the edit summary is correct
324 $savedSummary = $wikiPage->getRevisionRecord()->getComment( RevisionRecord::RAW )->text;
325 $this->assertSame( $expectedSummary, $savedSummary, 'Correct summary saved (existing page)' );
328 public static function provideEditNewSectionSummarySectiontitle() {
329 $sectiontitleCases = [
330 'unset' => null,
331 'empty' => '',
332 'set' => 'sectiontitle',
334 $summaryCases = [
335 'unset' => null,
336 'empty' => '',
337 'set' => 'summary',
340 $expectedTexts = [
341 "text",
342 "text",
343 "== summary ==\n\ntext",
344 "text",
345 "text",
346 "text",
347 "== sectiontitle ==\n\ntext",
348 "== sectiontitle ==\n\ntext",
349 "== sectiontitle ==\n\ntext",
352 $expectedSummaries = [
355 '/* summary */ new section',
358 'summary',
359 '/* sectiontitle */ new section',
360 '/* sectiontitle */ new section',
361 'summary',
364 $i = 0;
365 foreach ( $sectiontitleCases as $sectiontitleDesc => $sectiontitle ) {
366 foreach ( $summaryCases as $summaryDesc => $summary ) {
367 $message = "sectiontitle $sectiontitleDesc, summary $summaryDesc";
368 yield $message => [
369 $sectiontitle,
370 $summary,
371 $expectedTexts[$i],
372 $expectedSummaries[$i],
374 $i++;
380 * Ensure we can edit through a redirect, if adding a section
382 public function testEdit_redirect() {
383 static $count = 0;
384 $count++;
386 // assume NS_HELP defaults to wikitext
387 $title = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEdit_redirect_$count" );
388 $wikiPageFactory = $this->getServiceContainer()->getWikiPageFactory();
389 $page = $this->getExistingTestPage( $title );
390 $this->forceRevisionDate( $page, '20120101000000' );
392 $rtitle = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEdit_redirect_r$count" );
393 $rpage = $wikiPageFactory->newFromTitle( $rtitle );
395 $baseTime = $page->getRevisionRecord()->getTimestamp();
397 // base edit for redirect
398 $rpage->doUserEditContent(
399 new WikitextContent( "#REDIRECT [[{$title->getPrefixedText()}]]" ),
400 $this->getTestSysop()->getUser(),
401 "testing 1",
402 EDIT_NEW
404 $this->forceRevisionDate( $rpage, '20120101000000' );
406 // conflicting edit to redirect
407 $rpage->doUserEditContent(
408 new WikitextContent( "#REDIRECT [[{$title->getPrefixedText()}]]\n\n[[Category:Test]]" ),
409 $this->getTestUser()->getUser(),
410 "testing 2",
411 EDIT_UPDATE
413 $this->forceRevisionDate( $rpage, '20120101020202' );
415 // try to save edit, following the redirect
416 [ $re, , ] = $this->doApiRequestWithToken( [
417 'action' => 'edit',
418 'title' => $rtitle->getPrefixedText(),
419 'text' => 'nix bar!',
420 'basetimestamp' => $baseTime,
421 'section' => 'new',
422 'redirect' => true,
423 ] );
425 $this->assertSame( 'Success', $re['edit']['result'],
426 "no problems expected when following redirect" );
430 * Ensure we cannot edit through a redirect, if attempting to overwrite content
432 public function testEdit_redirectText() {
433 static $count = 0;
434 $count++;
436 // assume NS_HELP defaults to wikitext
437 $title = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEdit_redirectText_$count" );
438 $wikiPageFactory = $this->getServiceContainer()->getWikiPageFactory();
439 $page = $this->getExistingTestPage( $title );
440 $this->forceRevisionDate( $page, '20120101000000' );
441 $baseTime = $page->getRevisionRecord()->getTimestamp();
443 $rtitle = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEdit_redirectText_r$count" );
444 $rpage = $wikiPageFactory->newFromTitle( $rtitle );
446 // base edit for redirect
447 $rpage->doUserEditContent(
448 new WikitextContent( "#REDIRECT [[{$title->getPrefixedText()}]]" ),
449 $this->getTestSysop()->getUser(),
450 "testing 1",
451 EDIT_NEW
453 $this->forceRevisionDate( $rpage, '20120101000000' );
455 // conflicting edit to redirect
456 $rpage->doUserEditContent(
457 new WikitextContent( "#REDIRECT [[{$title->getPrefixedText()}]]\n\n[[Category:Test]]" ),
458 $this->getTestUser()->getUser(),
459 "testing 2",
460 EDIT_UPDATE
462 $this->forceRevisionDate( $rpage, '20120101020202' );
464 // try to save edit, following the redirect but without creating a section
465 try {
466 $this->doApiRequestWithToken( [
467 'action' => 'edit',
468 'title' => $rtitle->getPrefixedText(),
469 'text' => 'nix bar!',
470 'basetimestamp' => $baseTime,
471 'redirect' => true,
472 ] );
474 $this->fail( 'redirect-appendonly error expected' );
475 } catch ( ApiUsageException $ex ) {
476 $this->assertApiErrorCode( 'redirect-appendonly', $ex );
480 public function testEditConflict_revid() {
481 static $count = 0;
482 $count++;
484 // assume NS_HELP defaults to wikitext
485 $title = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEditConflict_$count" );
487 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
489 // base edit
490 $page->doUserEditContent(
491 new WikitextContent( "Foo" ),
492 $this->getTestSysop()->getUser(),
493 "testing 1",
494 EDIT_NEW
496 $this->forceRevisionDate( $page, '20120101000000' );
497 $baseId = $page->getRevisionRecord()->getId();
499 // conflicting edit
500 $page->doUserEditContent(
501 new WikitextContent( "Foo bar" ),
502 $this->getTestUser()->getUser(),
503 "testing 2",
504 EDIT_UPDATE
506 $this->forceRevisionDate( $page, '20120101020202' );
508 // try to save edit, expect conflict
509 try {
510 $this->doApiRequestWithToken( [
511 'action' => 'edit',
512 'title' => $title->getPrefixedText(),
513 'text' => 'nix bar!',
514 'baserevid' => $baseId,
515 ], null, $this->getTestSysop()->getUser() );
517 $this->fail( 'edit conflict expected' );
518 } catch ( ApiUsageException $ex ) {
519 $this->assertApiErrorCode( 'editconflict', $ex );
523 public function testEditConflict_timestamp() {
524 static $count = 0;
525 $count++;
527 // assume NS_HELP defaults to wikitext
528 $title = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEditConflict_$count" );
530 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
532 // base edit
533 $page->doUserEditContent(
534 new WikitextContent( "Foo" ),
535 $this->getTestSysop()->getUser(),
536 "testing 1",
537 EDIT_NEW
539 $this->forceRevisionDate( $page, '20120101000000' );
540 $baseTime = $page->getRevisionRecord()->getTimestamp();
542 // conflicting edit
543 $page->doUserEditContent(
544 new WikitextContent( "Foo bar" ),
545 $this->getTestUser()->getUser(),
546 "testing 2",
547 EDIT_UPDATE
549 $this->forceRevisionDate( $page, '20120101020202' );
551 // try to save edit, expect conflict
552 try {
553 $this->doApiRequestWithToken( [
554 'action' => 'edit',
555 'title' => $title->getPrefixedText(),
556 'text' => 'nix bar!',
557 'basetimestamp' => $baseTime,
558 ] );
560 $this->fail( 'edit conflict expected' );
561 } catch ( ApiUsageException $ex ) {
562 $this->assertApiErrorCode( 'editconflict', $ex );
567 * Ensure that editing using section=new will prevent simple conflicts
569 public function testEditConflict_newSection() {
570 static $count = 0;
571 $count++;
573 // assume NS_HELP defaults to wikitext
574 $title = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEditConflict_newSection_$count" );
576 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
578 // base edit
579 $page->doUserEditContent(
580 new WikitextContent( "Foo" ),
581 $this->getTestSysop()->getUser(),
582 "testing 1",
583 EDIT_NEW
585 $this->forceRevisionDate( $page, '20120101000000' );
586 $baseTime = $page->getRevisionRecord()->getTimestamp();
588 // conflicting edit
589 $page->doUserEditContent(
590 new WikitextContent( "Foo bar" ),
591 $this->getTestUser()->getUser(),
592 "testing 2",
593 EDIT_UPDATE
595 $this->forceRevisionDate( $page, '20120101020202' );
597 // try to save edit, expect no conflict
598 [ $re, , ] = $this->doApiRequestWithToken( [
599 'action' => 'edit',
600 'title' => $title->getPrefixedText(),
601 'text' => 'nix bar!',
602 'basetimestamp' => $baseTime,
603 'section' => 'new',
604 ] );
606 $this->assertSame( 'Success', $re['edit']['result'],
607 "no edit conflict expected here" );
610 public function testEditConflict_T43990() {
611 static $count = 0;
612 $count++;
615 * T43990: if the target page has a newer revision than the redirect, then editing the
616 * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously
617 * caused an edit conflict to be detected.
620 // assume NS_HELP defaults to wikitext
621 $title = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEditConflict_redirect_T43990_$count" );
622 $wikiPageFactory = $this->getServiceContainer()->getWikiPageFactory();
623 $page = $this->getExistingTestPage( $title );
624 $this->forceRevisionDate( $page, '20120101000000' );
626 $rtitle = Title::makeTitle( NS_HELP, "ApiEditPageTest_testEditConflict_redirect_T43990_r$count" );
627 $rpage = $wikiPageFactory->newFromTitle( $rtitle );
629 // base edit for redirect
630 $rpage->doUserEditContent(
631 new WikitextContent( "#REDIRECT [[{$title->getPrefixedText()}]]" ),
632 $this->getTestSysop()->getUser(),
633 "testing 1",
634 EDIT_NEW
636 $this->forceRevisionDate( $rpage, '20120101000000' );
638 // new edit to content
639 $page->doUserEditContent(
640 new WikitextContent( "Foo bar" ),
641 $this->getTestUser()->getUser(),
642 "testing 2",
643 EDIT_UPDATE
645 $this->forceRevisionDate( $rpage, '20120101020202' );
647 // try to save edit; should work, following the redirect.
648 [ $re, , ] = $this->doApiRequestWithToken( [
649 'action' => 'edit',
650 'title' => $rtitle->getPrefixedText(),
651 'text' => 'nix bar!',
652 'section' => 'new',
653 'redirect' => true,
654 ] );
656 $this->assertSame( 'Success', $re['edit']['result'],
657 "no edit conflict expected here" );
661 * @param WikiPage $page
662 * @param string|int $timestamp
664 protected function forceRevisionDate( WikiPage $page, $timestamp ) {
665 $dbw = $this->getDb();
667 $dbw->newUpdateQueryBuilder()
668 ->update( 'revision' )
669 ->set( [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ] )
670 ->where( [ 'rev_id' => $page->getLatest() ] )
671 ->caller( __METHOD__ )->execute();
673 $page->clear();
676 public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
677 $this->expectApiErrorCode( 'no-direct-editing' );
679 $this->doApiRequestWithToken( [
680 'action' => 'edit',
681 'title' => 'Dummy:ApiEditPageTest_nonTextPageEdit',
682 'text' => '{"animals":["kittens!"]}'
683 ] );
686 public function testSupportsDirectApiEditing_withContentHandlerOverride() {
687 $name = 'DummyNonText:ApiEditPageTest_testNonTextEdit';
688 $data = 'some bla bla text';
690 $result = $this->doApiRequestWithToken( [
691 'action' => 'edit',
692 'title' => $name,
693 'text' => $data,
694 ] );
696 $apiResult = $result[0];
698 // Validate API result data
699 $this->assertArrayHasKey( 'edit', $apiResult );
700 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
701 $this->assertSame( 'Success', $apiResult['edit']['result'] );
703 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
704 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
706 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
708 // validate resulting revision
709 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( Title::newFromText( $name ) );
710 $this->assertSame( "testing-nontext", $page->getContentModel() );
711 $this->assertSame( $data, $page->getContent()->serialize() );
715 * This test verifies that after changing the content model
716 * of a page, undoing that edit via the API will also
717 * undo the content model change.
719 public function testUndoAfterContentModelChange() {
720 $name = 'Help:' . __FUNCTION__;
721 $sysop = $this->getTestSysop()->getUser();
722 $otherUser = $this->getTestUser()->getUser();
724 $apiResult = $this->doApiRequestWithToken( [
725 'action' => 'edit',
726 'title' => $name,
727 'text' => 'some text',
728 ], null, $sysop )[0];
730 // Check success
731 $this->assertArrayHasKey( 'edit', $apiResult );
732 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
733 $this->assertSame( 'Success', $apiResult['edit']['result'] );
734 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
735 // Content model is wikitext
736 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
738 // Convert the page to JSON
739 $apiResult = $this->doApiRequestWithToken( [
740 'action' => 'edit',
741 'title' => $name,
742 'text' => '{}',
743 'contentmodel' => 'json',
744 ], null, $otherUser )[0];
746 // Check success
747 $this->assertArrayHasKey( 'edit', $apiResult );
748 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
749 $this->assertSame( 'Success', $apiResult['edit']['result'] );
750 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
751 $this->assertSame( 'json', $apiResult['edit']['contentmodel'] );
753 $apiResult = $this->doApiRequestWithToken( [
754 'action' => 'edit',
755 'title' => $name,
756 'undo' => $apiResult['edit']['newrevid']
757 ], null, $sysop )[0];
759 // Check success
760 $this->assertArrayHasKey( 'edit', $apiResult );
761 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
762 $this->assertSame( 'Success', $apiResult['edit']['result'] );
763 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
764 // Check that the contentmodel is back to wikitext now.
765 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
768 // The tests below are mostly not commented because they do exactly what
769 // you'd expect from the name.
771 public function testCorrectContentFormat() {
772 $title = Title::makeTitle( NS_HELP, 'TestCorrectContentFormat' );
774 $this->doApiRequestWithToken( [
775 'action' => 'edit',
776 'title' => $title->getPrefixedText(),
777 'text' => 'some text',
778 'contentmodel' => 'wikitext',
779 'contentformat' => 'text/x-wiki',
780 ] );
782 $this->assertTrue( $title->exists( IDBAccessObject::READ_LATEST ) );
785 public function testUnsupportedContentFormat() {
786 $title = Title::makeTitle( NS_HELP, 'TestUnsupportedContentFormat' );
788 $this->expectApiErrorCode( 'badvalue' );
790 try {
791 $this->doApiRequestWithToken( [
792 'action' => 'edit',
793 'title' => $title->getPrefixedText(),
794 'text' => 'some text',
795 'contentformat' => 'nonexistent format',
796 ] );
797 } finally {
798 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
802 public function testMismatchedContentFormat() {
803 $title = Title::makeTitle( NS_HELP, 'TestMismatchedContentFormat' );
805 $this->expectApiErrorCode( 'badformat' );
807 try {
808 $this->doApiRequestWithToken( [
809 'action' => 'edit',
810 'title' => $title->getPrefixedText(),
811 'text' => 'some text',
812 'contentmodel' => 'wikitext',
813 'contentformat' => 'text/plain',
814 ] );
815 } finally {
816 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
820 public function testUndoToInvalidRev() {
821 $title = Title::makeTitle( NS_HELP, 'TestUndoToInvalidRev' );
823 $revId = $this->editPage( $title, 'Some text' )->getNewRevision()
824 ->getId();
825 $revId++;
827 $this->expectApiErrorCode( 'nosuchrevid' );
829 $this->doApiRequestWithToken( [
830 'action' => 'edit',
831 'title' => $title->getPrefixedText(),
832 'undo' => $revId,
833 ] );
837 * Tests what happens if the undo parameter is a valid revision, but
838 * the undoafter parameter doesn't refer to a revision that exists in the
839 * database.
841 public function testUndoAfterToInvalidRev() {
842 // We can't just pick a large number for undoafter (as in
843 // testUndoToInvalidRev above), because then MediaWiki will helpfully
844 // assume we switched around undo and undoafter and we'll test the code
845 // path for undo being invalid, not undoafter. So instead we delete
846 // the revision from the database. In real life this case could come
847 // up if a revision number was skipped, e.g., if two transactions try
848 // to insert new revision rows at once and the first one to succeed
849 // gets rolled back.
850 $page = $this->getServiceContainer()->getWikiPageFactory()
851 ->newFromLinkTarget( new TitleValue( NS_HELP, 'TestUndoAfterToInvalidRev' ) );
853 $revId1 = $this->editPage( $page, '1' )->getNewRevision()->getId();
854 $revId2 = $this->editPage( $page, '2' )->getNewRevision()->getId();
855 $revId3 = $this->editPage( $page, '3' )->getNewRevision()->getId();
857 // Make the middle revision disappear
858 $dbw = $this->getDb();
859 $dbw->newDeleteQueryBuilder()
860 ->deleteFrom( 'revision' )
861 ->where( [ 'rev_id' => $revId2 ] )
862 ->caller( __METHOD__ )->execute();
863 $dbw->newUpdateQueryBuilder()
864 ->update( 'revision' )
865 ->set( [ 'rev_parent_id' => $revId1 ] )
866 ->where( [ 'rev_id' => $revId3 ] )
867 ->caller( __METHOD__ )->execute();
869 $this->expectApiErrorCode( 'nosuchrevid' );
871 $this->doApiRequestWithToken( [
872 'action' => 'edit',
873 'title' => $page->getTitle()->getPrefixedText(),
874 'undo' => $revId3,
875 'undoafter' => $revId2,
876 ] );
880 * Tests what happens if the undo parameter is a valid revision, but
881 * undoafter is hidden (rev_deleted).
883 public function testUndoAfterToHiddenRev() {
884 $page = $this->getServiceContainer()->getWikiPageFactory()
885 ->newFromLinkTarget( new TitleValue( NS_HELP, 'TestUndoAfterToHiddenRev' ) );
886 $titleObj = $page->getTitle();
888 $this->editPage( $page, '0' );
890 $revId1 = $this->editPage( $page, '1' )->getNewRevision()->getId();
892 $revId2 = $this->editPage( $page, '2' )->getNewRevision()->getId();
894 // Hide the middle revision
895 $list = RevisionDeleter::createList( 'revision',
896 RequestContext::getMain(), $titleObj, [ $revId1 ] );
897 // Set a user for modifying the visibility, this is needed because
898 // setVisibility generates a log, which cannot be an anonymous user actor
899 // when temporary accounts are enabled.
900 RequestContext::getMain()->setUser( $this->getTestUser()->getUser() );
901 $list->setVisibility( [
902 'value' => [ RevisionRecord::DELETED_TEXT => 1 ],
903 'comment' => 'Bye-bye',
904 ] );
906 $this->expectApiErrorCode( 'nosuchrevid' );
908 $this->doApiRequestWithToken( [
909 'action' => 'edit',
910 'title' => $titleObj->getPrefixedText(),
911 'undo' => $revId2,
912 'undoafter' => $revId1,
913 ] );
917 * Test undo when a revision with a higher id has an earlier timestamp.
918 * This can happen if importing an old revision.
920 public function testUndoWithSwappedRevisions() {
921 $this->markTestSkippedIfNoDiff3();
923 $page = $this->getServiceContainer()->getWikiPageFactory()
924 ->newFromLinkTarget( new TitleValue( NS_HELP, 'TestUndoWithSwappedRevisions' ) );
925 $this->editPage( $page, '0' );
927 $revId2 = $this->editPage( $page, '2' )->getNewRevision()->getId();
929 $revId1 = $this->editPage( $page, '1' )->getNewRevision()->getId();
931 // Now monkey with the timestamp
932 $dbw = $this->getDb();
933 $dbw->newUpdateQueryBuilder()
934 ->update( 'revision' )
935 ->set( [ 'rev_timestamp' => $dbw->timestamp( time() - 86400 ) ] )
936 ->where( [ 'rev_id' => $revId1 ] )
937 ->caller( __METHOD__ )->execute();
939 $this->doApiRequestWithToken( [
940 'action' => 'edit',
941 'title' => $page->getTitle()->getPrefixedText(),
942 'undo' => $revId2,
943 'undoafter' => $revId1,
944 ] );
946 $page->loadPageData( IDBAccessObject::READ_LATEST );
947 $this->assertSame( '1', $page->getContent()->getText() );
950 public function testUndoWithConflicts() {
951 $this->expectApiErrorCode( 'undofailure' );
953 $page = $this->getServiceContainer()->getWikiPageFactory()
954 ->newFromLinkTarget( new TitleValue( NS_HELP, 'TestUndoWithConflicts' ) );
955 $this->editPage( $page, '1' );
957 $revId = $this->editPage( $page, '2' )->getNewRevision()->getId();
959 $this->editPage( $page, '3' );
961 $this->doApiRequestWithToken( [
962 'action' => 'edit',
963 'title' => $page->getTitle()->getPrefixedText(),
964 'undo' => $revId,
965 ] );
967 $page->loadPageData( IDBAccessObject::READ_LATEST );
968 $this->assertSame( '3', $page->getContent()->getText() );
971 public function testReversedUndoAfter() {
972 $this->markTestSkippedIfNoDiff3();
974 $page = $this->getServiceContainer()->getWikiPageFactory()
975 ->newFromLinkTarget( new TitleValue( NS_HELP, 'TestReversedUndoAfter' ) );
976 $this->editPage( $page, '0' );
977 $revId1 = $this->editPage( $page, '1' )->getNewRevision()->getId();
978 $revId2 = $this->editPage( $page, '2' )->getNewRevision()->getId();
980 $this->doApiRequestWithToken( [
981 'action' => 'edit',
982 'title' => $page->getTitle()->getPrefixedText(),
983 'undo' => $revId1,
984 'undoafter' => $revId2,
985 ] );
987 $page->loadPageData( IDBAccessObject::READ_LATEST );
988 $this->assertSame( '2', $page->getContent()->getText() );
991 public function testUndoToRevFromDifferentPage() {
992 $title1 = Title::makeTitle( NS_HELP, 'TestUndoToRevFromDifferentPage-1' );
993 $this->editPage( $title1, 'Some text' );
994 $revId = $this->editPage( $title1, 'Some more text' )
995 ->getNewRevision()->getId();
997 $title2 = Title::makeTitle( NS_HELP, 'TestUndoToRevFromDifferentPage-2' );
998 $this->editPage( $title2, 'Some text' );
1000 $this->expectApiErrorCode( 'revwrongpage' );
1002 $this->doApiRequestWithToken( [
1003 'action' => 'edit',
1004 'title' => $title2->getPrefixedText(),
1005 'undo' => $revId,
1006 ] );
1009 public function testUndoAfterToRevFromDifferentPage() {
1010 $title1 = Title::makeTitle( NS_HELP, 'TestUndoAfterToRevFromDifferentPage-1' );
1011 $revId1 = $this->editPage( $title1, 'Some text' )
1012 ->getNewRevision()->getId();
1014 $title2 = Title::makeTitle( NS_HELP, 'TestUndoAfterToRevFromDifferentPage-2' );
1015 $revId2 = $this->editPage( $title2, 'Some text' )
1016 ->getNewRevision()->getId();
1018 $this->expectApiErrorCode( 'revwrongpage' );
1020 $this->doApiRequestWithToken( [
1021 'action' => 'edit',
1022 'title' => $title2->getPrefixedText(),
1023 'undo' => $revId2,
1024 'undoafter' => $revId1,
1025 ] );
1028 public function testMd5Text() {
1029 $title = Title::makeTitle( NS_HELP, 'TestMd5Text' );
1031 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
1033 $this->doApiRequestWithToken( [
1034 'action' => 'edit',
1035 'title' => $title->getPrefixedText(),
1036 'text' => 'Some text',
1037 'md5' => md5( 'Some text' ),
1038 ] );
1040 $this->assertTrue( $title->exists( IDBAccessObject::READ_LATEST ) );
1043 public function testMd5PrependText() {
1044 $title = Title::makeTitle( NS_HELP, 'TestMd5PrependText' );
1046 $this->editPage( $title, 'Some text' );
1048 $this->doApiRequestWithToken( [
1049 'action' => 'edit',
1050 'title' => $title->getPrefixedText(),
1051 'prependtext' => 'Alert: ',
1052 'md5' => md5( 'Alert: ' ),
1053 ] );
1055 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1056 ->getContent()->getText();
1057 $this->assertSame( 'Alert: Some text', $text );
1060 public function testMd5AppendText() {
1061 $title = Title::makeTitle( NS_HELP, 'TestMd5AppendText' );
1063 $this->editPage( $title, 'Some text' );
1065 $this->doApiRequestWithToken( [
1066 'action' => 'edit',
1067 'title' => $title->getPrefixedText(),
1068 'appendtext' => ' is nice',
1069 'md5' => md5( ' is nice' ),
1070 ] );
1072 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1073 ->getContent()->getText();
1074 $this->assertSame( 'Some text is nice', $text );
1077 public function testMd5PrependAndAppendText() {
1078 $title = Title::makeTitle( NS_HELP, 'TestMd5PrependAndAppendText' );
1080 $this->editPage( $title, 'Some text' );
1082 $this->doApiRequestWithToken( [
1083 'action' => 'edit',
1084 'title' => $title->getPrefixedText(),
1085 'prependtext' => 'Alert: ',
1086 'appendtext' => ' is nice',
1087 'md5' => md5( 'Alert: is nice' ),
1088 ] );
1090 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1091 ->getContent()->getText();
1092 $this->assertSame( 'Alert: Some text is nice', $text );
1095 public function testIncorrectMd5Text() {
1096 $name = 'Help:' . ucfirst( __FUNCTION__ );
1098 $this->expectApiErrorCode( 'badmd5' );
1100 $this->doApiRequestWithToken( [
1101 'action' => 'edit',
1102 'title' => $name,
1103 'text' => 'Some text',
1104 'md5' => md5( '' ),
1105 ] );
1108 public function testIncorrectMd5PrependText() {
1109 $name = 'Help:' . ucfirst( __FUNCTION__ );
1111 $this->expectApiErrorCode( 'badmd5' );
1113 $this->doApiRequestWithToken( [
1114 'action' => 'edit',
1115 'title' => $name,
1116 'prependtext' => 'Some ',
1117 'appendtext' => 'text',
1118 'md5' => md5( 'Some ' ),
1119 ] );
1122 public function testIncorrectMd5AppendText() {
1123 $name = 'Help:' . ucfirst( __FUNCTION__ );
1125 $this->expectApiErrorCode( 'badmd5' );
1127 $this->doApiRequestWithToken( [
1128 'action' => 'edit',
1129 'title' => $name,
1130 'prependtext' => 'Some ',
1131 'appendtext' => 'text',
1132 'md5' => md5( 'text' ),
1133 ] );
1136 public function testCreateOnly() {
1137 $title = Title::makeTitle( NS_HELP, 'TestCreateOnly' );
1139 $this->expectApiErrorCode( 'articleexists' );
1141 $this->editPage( $title, 'Some text' );
1142 $this->assertTrue( $title->exists( IDBAccessObject::READ_LATEST ) );
1144 try {
1145 $this->doApiRequestWithToken( [
1146 'action' => 'edit',
1147 'title' => $title->getPrefixedText(),
1148 'text' => 'Some more text',
1149 'createonly' => '',
1150 ] );
1151 } finally {
1152 // Validate that content was not changed
1153 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1154 ->getContent()->getText();
1156 $this->assertSame( 'Some text', $text );
1160 public function testNoCreate() {
1161 $title = Title::makeTitle( NS_HELP, 'TestNoCreate' );
1163 $this->expectApiErrorCode( 'missingtitle' );
1165 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
1167 try {
1168 $this->doApiRequestWithToken( [
1169 'action' => 'edit',
1170 'title' => $title->getPrefixedText(),
1171 'text' => 'Some text',
1172 'nocreate' => '',
1173 ] );
1174 } finally {
1175 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
1180 * Appending/prepending is currently only supported for TextContent. We
1181 * test this right now, and when support is added this test should be
1182 * replaced by tests that the support is correct.
1184 public function testAppendWithNonTextContentHandler() {
1185 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1187 $this->expectApiErrorCode( 'appendnotsupported' );
1189 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1190 static function ( Title $title, &$model ) use ( $name ) {
1191 if ( $title->getPrefixedText() === $name ) {
1192 $model = 'testing-nontext';
1194 return true;
1198 $this->doApiRequestWithToken( [
1199 'action' => 'edit',
1200 'title' => $name,
1201 'appendtext' => 'Some text',
1202 ] );
1205 public function testAppendInMediaWikiNamespace() {
1206 $title = Title::makeTitle( NS_MEDIAWIKI, 'TestAppendInMediaWikiNamespace' );
1208 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
1210 $this->doApiRequestWithToken( [
1211 'action' => 'edit',
1212 'title' => $title->getPrefixedText(),
1213 'appendtext' => 'Some text',
1214 ] );
1216 $this->assertTrue( $title->exists( IDBAccessObject::READ_LATEST ) );
1219 public function testAppendInMediaWikiNamespaceWithSerializationError() {
1220 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1222 $this->expectApiErrorCode( 'parseerror' );
1224 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1225 static function ( Title $title, &$model ) use ( $name ) {
1226 if ( $title->getPrefixedText() === $name ) {
1227 $model = 'testing-serialize-error';
1229 return true;
1233 $this->doApiRequestWithToken( [
1234 'action' => 'edit',
1235 'title' => $name,
1236 'appendtext' => 'Some text',
1237 ] );
1240 public function testAppendNewSection() {
1241 $title = Title::makeTitle( NS_HELP, 'TestAppendNewSection' );
1243 $this->editPage( $title, 'Initial content' );
1245 $this->doApiRequestWithToken( [
1246 'action' => 'edit',
1247 'title' => $title->getPrefixedText(),
1248 'appendtext' => '== New section ==',
1249 'section' => 'new',
1250 ] );
1252 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1253 ->getContent()->getText();
1255 $this->assertSame( "Initial content\n\n== New section ==", $text );
1258 public function testAppendNewSectionWithInvalidContentModel() {
1259 $title = Title::makeTitle( NS_HELP, 'TestAppendNewSectionWithInvalidContentModel' );
1261 $this->expectApiErrorCode( 'sectionsnotsupported' );
1263 $this->editPage( $title, 'Initial content' );
1265 $this->doApiRequestWithToken( [
1266 'action' => 'edit',
1267 'title' => $title->getPrefixedText(),
1268 'appendtext' => '== New section ==',
1269 'section' => 'new',
1270 'contentmodel' => 'text',
1271 ] );
1274 public function testAppendNewSectionWithTitle() {
1275 $title = Title::makeTitle( NS_HELP, 'TestAppendNewSectionWithTitle' );
1277 $this->editPage( $title, 'Initial content' );
1279 $this->doApiRequestWithToken( [
1280 'action' => 'edit',
1281 'title' => $title->getPrefixedText(),
1282 'sectiontitle' => 'My section',
1283 'appendtext' => 'More content',
1284 'section' => 'new',
1285 ] );
1287 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
1289 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1290 $page->getContent()->getText() );
1291 $comment = $page->getRevisionRecord()->getComment();
1292 $this->assertInstanceOf( CommentStoreComment::class, $comment );
1293 $this->assertSame( '/* My section */ new section', $comment->text );
1296 public function testAppendNewSectionWithSummary() {
1297 $title = Title::makeTitle( NS_HELP, 'TestAppendNewSectionWithSummary' );
1299 $this->editPage( $title, 'Initial content' );
1301 $this->doApiRequestWithToken( [
1302 'action' => 'edit',
1303 'title' => $title->getPrefixedText(),
1304 'appendtext' => 'More content',
1305 'section' => 'new',
1306 'summary' => 'Add new section',
1307 ] );
1309 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
1311 $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content",
1312 $page->getContent()->getText() );
1313 // EditPage actually assumes the summary is the section name here
1314 $comment = $page->getRevisionRecord()->getComment();
1315 $this->assertInstanceOf( CommentStoreComment::class, $comment );
1316 $this->assertSame( '/* Add new section */ new section', $comment->text );
1319 public function testAppendNewSectionWithTitleAndSummary() {
1320 $title = Title::makeTitle( NS_HELP, 'TestAppendNewSectionWithTitleAndSummary' );
1322 $this->editPage( $title, 'Initial content' );
1324 $this->doApiRequestWithToken( [
1325 'action' => 'edit',
1326 'title' => $title->getPrefixedText(),
1327 'sectiontitle' => 'My section',
1328 'appendtext' => 'More content',
1329 'section' => 'new',
1330 'summary' => 'Add new section',
1331 ] );
1333 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
1335 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1336 $page->getContent()->getText() );
1337 $comment = $page->getRevisionRecord()->getComment();
1338 $this->assertInstanceOf( CommentStoreComment::class, $comment );
1339 $this->assertSame( 'Add new section', $comment->text );
1342 public function testAppendToSection() {
1343 $title = Title::makeTitle( NS_HELP, 'TestAppendToSection' );
1345 $this->editPage( $title, "== Section 1 ==\n\nContent\n\n" .
1346 "== Section 2 ==\n\nFascinating!" );
1348 $this->doApiRequestWithToken( [
1349 'action' => 'edit',
1350 'title' => $title->getPrefixedText(),
1351 'appendtext' => ' and more content',
1352 'section' => '1',
1353 ] );
1355 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1356 ->getContent()->getText();
1358 $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" .
1359 "== Section 2 ==\n\nFascinating!", $text );
1362 public function testAppendToFirstSection() {
1363 $title = Title::makeTitle( NS_HELP, 'TestAppendToFirstSection' );
1365 $this->editPage( $title, "Content\n\n== Section 1 ==\n\nFascinating!" );
1367 $this->doApiRequestWithToken( [
1368 'action' => 'edit',
1369 'title' => $title->getPrefixedText(),
1370 'appendtext' => ' and more content',
1371 'section' => '0',
1372 ] );
1374 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1375 ->getContent()->getText();
1377 $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" .
1378 "Fascinating!", $text );
1381 public function testAppendToNonexistentSection() {
1382 $title = Title::makeTitle( NS_HELP, 'TestAppendToNonexistentSection' );
1384 $this->expectApiErrorCode( 'nosuchsection' );
1386 $this->editPage( $title, 'Content' );
1388 try {
1389 $this->doApiRequestWithToken( [
1390 'action' => 'edit',
1391 'title' => $title->getPrefixedText(),
1392 'appendtext' => ' and more content',
1393 'section' => '1',
1394 ] );
1395 } finally {
1396 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1397 ->getContent()->getText();
1399 $this->assertSame( 'Content', $text );
1403 public function testEditMalformedSection() {
1404 $title = Title::makeTitle( NS_HELP, 'TestEditMalformedSection' );
1406 $this->expectApiErrorCode( 'invalidsection' );
1407 $this->editPage( $title, 'Content' );
1409 try {
1410 $this->doApiRequestWithToken( [
1411 'action' => 'edit',
1412 'title' => $title->getPrefixedText(),
1413 'text' => 'Different content',
1414 'section' => 'It is unlikely that this is valid',
1415 ] );
1416 } finally {
1417 $text = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title )
1418 ->getContent()->getText();
1420 $this->assertSame( 'Content', $text );
1424 public function testEditWithStartTimestamp() {
1425 $title = Title::makeTitle( NS_HELP, 'TestEditWithStartTimestamp' );
1426 $this->expectApiErrorCode( 'pagedeleted' );
1428 $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
1430 $this->editPage( $title, 'Some text' );
1432 $pageObj = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
1433 $this->deletePage( $pageObj );
1435 $this->assertFalse( $pageObj->exists() );
1437 try {
1438 $this->doApiRequestWithToken( [
1439 'action' => 'edit',
1440 'title' => $title->getPrefixedText(),
1441 'text' => 'Different text',
1442 'starttimestamp' => $startTime,
1443 ] );
1444 } finally {
1445 $this->assertFalse( $pageObj->exists() );
1449 public function testEditMinor() {
1450 $title = Title::makeTitle( NS_HELP, 'TestEditMinor' );
1452 $this->editPage( $title, 'Some text' );
1454 $this->doApiRequestWithToken( [
1455 'action' => 'edit',
1456 'title' => $title->getPrefixedText(),
1457 'text' => 'Different text',
1458 'minor' => '',
1459 ] );
1461 $revisionStore = $this->getServiceContainer()->getRevisionStore();
1462 $revision = $revisionStore->getRevisionByTitle( $title );
1463 $this->assertTrue( $revision->isMinor() );
1466 public function testEditRecreate() {
1467 $title = Title::makeTitle( NS_HELP, 'TestEditRecreate' );
1469 $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
1471 $this->editPage( $title, 'Some text' );
1473 $pageObj = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
1474 $this->deletePage( $pageObj );
1476 $this->assertFalse( $pageObj->exists() );
1478 $this->doApiRequestWithToken( [
1479 'action' => 'edit',
1480 'title' => $title->getPrefixedText(),
1481 'text' => 'Different text',
1482 'starttimestamp' => $startTime,
1483 'recreate' => '',
1484 ] );
1486 $this->assertTrue( $title->exists( IDBAccessObject::READ_LATEST ) );
1489 public function testEditWatch() {
1490 $title = Title::makeTitle( NS_HELP, 'TestEditWatch' );
1491 $user = $this->getTestSysop()->getUser();
1492 $watchlistManager = $this->getServiceContainer()->getWatchlistManager();
1494 $this->doApiRequestWithToken( [
1495 'action' => 'edit',
1496 'title' => $title->getPrefixedText(),
1497 'text' => 'Some text',
1498 'watch' => '',
1499 'watchlistexpiry' => '99990123000000',
1500 ] );
1502 $this->assertTrue( $title->exists( IDBAccessObject::READ_LATEST ) );
1503 $this->assertTrue( $watchlistManager->isWatched( $user, $title ) );
1504 $this->assertTrue( $watchlistManager->isTempWatched( $user, $title ) );
1507 public function testEditUnwatch() {
1508 $title = Title::makeTitle( NS_HELP, 'TestEditUnwatch' );
1509 $user = $this->getTestSysop()->getUser();
1511 $watchlistManager = $this->getServiceContainer()->getWatchlistManager();
1512 $watchlistManager->addWatch( $user, $title );
1514 $this->assertFalse( $title->exists() );
1515 $this->assertTrue( $watchlistManager->isWatched( $user, $title ) );
1517 $this->doApiRequestWithToken( [
1518 'action' => 'edit',
1519 'title' => $title->getPrefixedText(),
1520 'text' => 'Some text',
1521 'unwatch' => '',
1522 ] );
1524 $this->assertTrue( $title->exists( IDBAccessObject::READ_LATEST ) );
1525 $this->assertFalse( $watchlistManager->isWatched( $user, $title ) );
1528 public function testEditWithTag() {
1529 $name = 'Help:' . ucfirst( __FUNCTION__ );
1531 $this->getServiceContainer()->getChangeTagsStore()->defineTag( 'custom tag' );
1533 $revId = $this->doApiRequestWithToken( [
1534 'action' => 'edit',
1535 'title' => $name,
1536 'text' => 'Some text',
1537 'tags' => 'custom tag',
1538 ] )[0]['edit']['newrevid'];
1540 $this->assertSame( 'custom tag', $this->getDb()->newSelectQueryBuilder()
1541 ->select( 'ctd_name' )
1542 ->from( 'change_tag' )
1543 ->join( 'change_tag_def', null, 'ctd_id = ct_tag_id' )
1544 ->where( [ 'ct_rev_id' => $revId ] )
1545 ->caller( __METHOD__ )->fetchField() );
1548 public function testEditWithoutTagPermission() {
1549 $title = Title::makeTitle( NS_HELP, 'TestEditWithoutTagPermission' );
1551 $this->expectApiErrorCode( 'tags-apply-no-permission' );
1553 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
1555 $this->getServiceContainer()->getChangeTagsStore()->defineTag( 'custom tag' );
1556 $this->overrideConfigValue(
1557 MainConfigNames::RevokePermissions,
1558 [ 'user' => [ 'applychangetags' => true ] ]
1561 try {
1562 $this->doApiRequestWithToken( [
1563 'action' => 'edit',
1564 'title' => $title->getPrefixedText(),
1565 'text' => 'Some text',
1566 'tags' => 'custom tag',
1567 ] );
1568 } finally {
1569 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
1573 public function testEditAbortedByEditPageHookWithResult() {
1574 $title = Title::makeTitle( NS_HELP, 'TestEditAbortedByEditPageHookWithResult' );
1576 $this->setTemporaryHook( 'EditFilterMergedContent',
1577 static function ( $unused1, $unused2, Status $status ) {
1578 $status->statusData = [ 'msg' => 'A message for you!' ];
1579 return false;
1580 } );
1582 $res = $this->doApiRequestWithToken( [
1583 'action' => 'edit',
1584 'title' => $title->getPrefixedText(),
1585 'text' => 'Some text',
1586 ] );
1588 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
1589 $this->assertSame( [ 'edit' => [ 'msg' => 'A message for you!',
1590 'result' => 'Failure' ] ], $res[0] );
1593 public function testEditAbortedByEditPageHookWithNoResult() {
1594 $title = Title::makeTitle( NS_HELP, 'TestEditAbortedByEditPageHookWithNoResult' );
1596 $this->expectApiErrorCode( 'hookaborted' );
1598 $this->setTemporaryHook( 'EditFilterMergedContent',
1599 static function () {
1600 return false;
1604 try {
1605 $this->doApiRequestWithToken( [
1606 'action' => 'edit',
1607 'title' => $title->getPrefixedText(),
1608 'text' => 'Some text',
1609 ] );
1610 } finally {
1611 $this->assertFalse( $title->exists( IDBAccessObject::READ_LATEST ) );
1615 public function testEditWhileBlocked() {
1616 $name = 'Help:' . ucfirst( __FUNCTION__ );
1618 $blockStore = $this->getServiceContainer()->getDatabaseBlockStore();
1619 $this->assertNull( $blockStore->newFromTarget( '127.0.0.1' ) );
1621 $user = $this->getTestSysop()->getUser();
1622 $block = new DatabaseBlock( [
1623 'address' => $user->getName(),
1624 'by' => $user,
1625 'reason' => 'Capriciousness',
1626 'timestamp' => '19370101000000',
1627 'expiry' => 'infinity',
1628 'enableAutoblock' => true,
1629 ] );
1630 $blockStore->insertBlock( $block );
1632 try {
1633 $this->doApiRequestWithToken( [
1634 'action' => 'edit',
1635 'title' => $name,
1636 'text' => 'Some text',
1637 ] );
1638 $this->fail( 'Expected exception not thrown' );
1639 } catch ( ApiUsageException $ex ) {
1640 $this->assertApiErrorCode( 'blocked', $ex );
1641 $this->assertNotNull( $blockStore->newFromTarget( '127.0.0.1' ), 'Autoblock spread' );
1645 public function testEditWhileReadOnly() {
1646 $name = 'Help:' . ucfirst( __FUNCTION__ );
1648 // Create the test user before making the DB readonly
1649 $user = $this->getTestSysop()->getUser();
1650 $this->expectApiErrorCode( 'readonly' );
1652 $svc = $this->getServiceContainer()->getReadOnlyMode();
1653 $svc->setReason( "Read-only for testing" );
1655 try {
1656 $this->doApiRequestWithToken( [
1657 'action' => 'edit',
1658 'title' => $name,
1659 'text' => 'Some text',
1660 ], null, $user );
1661 } finally {
1662 $svc->setReason( false );
1666 public function testCreateImageRedirectAnon() {
1667 $this->disableAutoCreateTempUser();
1668 $name = 'File:' . ucfirst( __FUNCTION__ );
1670 $this->expectApiErrorCode( 'noimageredirect-anon' );
1672 $this->doApiRequestWithToken( [
1673 'action' => 'edit',
1674 'title' => $name,
1675 'text' => '#REDIRECT [[File:Other file.png]]',
1676 ], null, new User() );
1679 public function testCreateImageRedirectLoggedIn() {
1680 $name = 'File:' . ucfirst( __FUNCTION__ );
1682 $this->expectApiErrorCode( 'noimageredirect' );
1684 $this->overrideConfigValue(
1685 MainConfigNames::RevokePermissions,
1686 [ 'user' => [ 'upload' => true ] ]
1689 $this->doApiRequestWithToken( [
1690 'action' => 'edit',
1691 'title' => $name,
1692 'text' => '#REDIRECT [[File:Other file.png]]',
1693 ] );
1696 public function testTooBigEdit() {
1697 $name = 'Help:' . ucfirst( __FUNCTION__ );
1699 $this->expectApiErrorCode( 'contenttoobig' );
1701 $this->overrideConfigValue( MainConfigNames::MaxArticleSize, 1 );
1703 $text = str_repeat( '!', 1025 );
1705 $this->doApiRequestWithToken( [
1706 'action' => 'edit',
1707 'title' => $name,
1708 'text' => $text,
1709 ] );
1712 public function testProhibitedAnonymousEdit() {
1713 $name = 'Help:' . ucfirst( __FUNCTION__ );
1715 $this->expectApiErrorCode( 'permissiondenied' );
1717 $this->overrideConfigValue(
1718 MainConfigNames::RevokePermissions,
1719 [ '*' => [ 'edit' => true ] ]
1722 $this->doApiRequestWithToken( [
1723 'action' => 'edit',
1724 'title' => $name,
1725 'text' => 'Some text',
1726 ], null, new User() );
1729 public function testProhibitedChangeContentModel() {
1730 $name = 'Help:' . ucfirst( __FUNCTION__ );
1732 $this->expectApiErrorCode( 'cantchangecontentmodel' );
1734 $this->overrideConfigValue(
1735 MainConfigNames::RevokePermissions,
1736 [ 'user' => [ 'editcontentmodel' => true ] ]
1739 $this->doApiRequestWithToken( [
1740 'action' => 'edit',
1741 'title' => $name,
1742 'text' => 'Some text',
1743 'contentmodel' => 'json',
1744 ] );
1747 public function testMidEditContentModelMismatch() {
1748 $title = Title::makeTitle( NS_HELP, 'TestMidEditContentModelMismatch' );
1750 $page = $this->getServiceContainer()->getWikiPageFactory()->newFromTitle( $title );
1752 // base edit, currently in Wikitext
1753 $page->doUserEditContent(
1754 new WikitextContent( "Foo" ),
1755 $this->getTestSysop()->getUser(),
1756 "testing 1",
1757 EDIT_NEW
1759 $this->forceRevisionDate( $page, '20120101000000' );
1760 $baseId = $page->getRevisionRecord()->getId();
1762 // Attempt edit in Javascript. This may happen, for instance, if we
1763 // started editing the base content while it was in Javascript and
1764 // before we save it was changed to Wikitext (base edit model).
1765 $page->doUserEditContent(
1766 new JavaScriptContent( "Bar" ),
1767 $this->getTestUser()->getUser(),
1768 "testing 2",
1769 EDIT_UPDATE
1771 $this->forceRevisionDate( $page, '20120101020202' );
1773 // ContentHandler may throw exception if we attempt saving the above, so we will
1774 // handle that with contentmodel-mismatch error. Test this is the case.
1775 try {
1776 $this->doApiRequestWithToken( [
1777 'action' => 'edit',
1778 'title' => $title->getPrefixedText(),
1779 'text' => 'different content models!',
1780 'baserevid' => $baseId,
1781 ] );
1782 $this->fail( "Should have raised an ApiUsageException" );
1783 } catch ( ApiUsageException $e ) {
1784 $this->assertApiErrorCode( 'contentmodel-mismatch', $e );