852812bef5272f9ab769b7e82ea2a71745a80361
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiEditPageTest.php
1 <?php
2
3 /**
4 * Tests for MediaWiki api.php?action=edit.
5 *
6 * @author Daniel Kinzler
7 *
8 * @group API
9 * @group Database
10 * @group medium
11 *
12 * @covers ApiEditPage
13 */
14 class ApiEditPageTest extends ApiTestCase {
15
16 protected function setUp() {
17 parent::setUp();
18
19 $this->setMwGlobals( [
20 'wgExtraNamespaces' => [
21 12312 => 'Dummy',
22 12313 => 'Dummy_talk',
23 12314 => 'DummyNonText',
24 12315 => 'DummyNonText_talk',
25 ],
26 'wgNamespaceContentModels' => [
27 12312 => 'testing',
28 12314 => 'testing-nontext',
29 ],
30 ] );
31 $this->mergeMwGlobalArrayValue( 'wgContentHandlers', [
32 'testing' => 'DummyContentHandlerForTesting',
33 'testing-nontext' => 'DummyNonTextContentHandler',
34 'testing-serialize-error' => 'DummySerializeErrorContentHandler',
35 ] );
36 }
37
38 public function testEdit() {
39 $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext
40
41 // -- test new page --------------------------------------------
42 $apiResult = $this->doApiRequestWithToken( [
43 'action' => 'edit',
44 'title' => $name,
45 'text' => 'some text',
46 ] );
47 $apiResult = $apiResult[0];
48
49 // Validate API result data
50 $this->assertArrayHasKey( 'edit', $apiResult );
51 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
52 $this->assertSame( 'Success', $apiResult['edit']['result'] );
53
54 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
55 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
56
57 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
58
59 // -- test existing page, no change ----------------------------
60 $data = $this->doApiRequestWithToken( [
61 'action' => 'edit',
62 'title' => $name,
63 'text' => 'some text',
64 ] );
65
66 $this->assertSame( 'Success', $data[0]['edit']['result'] );
67
68 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
69 $this->assertArrayHasKey( 'nochange', $data[0]['edit'] );
70
71 // -- test existing page, with change --------------------------
72 $data = $this->doApiRequestWithToken( [
73 'action' => 'edit',
74 'title' => $name,
75 'text' => 'different text'
76 ] );
77
78 $this->assertSame( 'Success', $data[0]['edit']['result'] );
79
80 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
81 $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] );
82
83 $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] );
84 $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] );
85 $this->assertNotEquals(
86 $data[0]['edit']['newrevid'],
87 $data[0]['edit']['oldrevid'],
88 "revision id should change after edit"
89 );
90 }
91
92 /**
93 * @return array
94 */
95 public static function provideEditAppend() {
96 return [
97 [ # 0: append
98 'foo', 'append', 'bar', "foobar"
99 ],
100 [ # 1: prepend
101 'foo', 'prepend', 'bar', "barfoo"
102 ],
103 [ # 2: append to empty page
104 '', 'append', 'foo', "foo"
105 ],
106 [ # 3: prepend to empty page
107 '', 'prepend', 'foo', "foo"
108 ],
109 [ # 4: append to non-existing page
110 null, 'append', 'foo', "foo"
111 ],
112 [ # 5: prepend to non-existing page
113 null, 'prepend', 'foo', "foo"
114 ],
115 ];
116 }
117
118 /**
119 * @dataProvider provideEditAppend
120 */
121 public function testEditAppend( $text, $op, $append, $expected ) {
122 static $count = 0;
123 $count++;
124
125 // assume NS_HELP defaults to wikitext
126 $name = "Help:ApiEditPageTest_testEditAppend_$count";
127
128 // -- create page (or not) -----------------------------------------
129 if ( $text !== null ) {
130 list( $re ) = $this->doApiRequestWithToken( [
131 'action' => 'edit',
132 'title' => $name,
133 'text' => $text, ] );
134
135 $this->assertSame( 'Success', $re['edit']['result'] ); // sanity
136 }
137
138 // -- try append/prepend --------------------------------------------
139 list( $re ) = $this->doApiRequestWithToken( [
140 'action' => 'edit',
141 'title' => $name,
142 $op . 'text' => $append, ] );
143
144 $this->assertSame( 'Success', $re['edit']['result'] );
145
146 // -- validate -----------------------------------------------------
147 $page = new WikiPage( Title::newFromText( $name ) );
148 $content = $page->getContent();
149 $this->assertNotNull( $content, 'Page should have been created' );
150
151 $text = $content->getNativeData();
152
153 $this->assertSame( $expected, $text );
154 }
155
156 /**
157 * Test editing of sections
158 */
159 public function testEditSection() {
160 $name = 'Help:ApiEditPageTest_testEditSection';
161 $page = WikiPage::factory( Title::newFromText( $name ) );
162 $text = "==section 1==\ncontent 1\n==section 2==\ncontent2";
163 // Preload the page with some text
164 $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), 'summary' );
165
166 list( $re ) = $this->doApiRequestWithToken( [
167 'action' => 'edit',
168 'title' => $name,
169 'section' => '1',
170 'text' => "==section 1==\nnew content 1",
171 ] );
172 $this->assertSame( 'Success', $re['edit']['result'] );
173 $newtext = WikiPage::factory( Title::newFromText( $name ) )
174 ->getContent( Revision::RAW )
175 ->getNativeData();
176 $this->assertSame( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext );
177
178 // Test that we raise a 'nosuchsection' error
179 try {
180 $this->doApiRequestWithToken( [
181 'action' => 'edit',
182 'title' => $name,
183 'section' => '9999',
184 'text' => 'text',
185 ] );
186 $this->fail( "Should have raised an ApiUsageException" );
187 } catch ( ApiUsageException $e ) {
188 $this->assertTrue( self::apiExceptionHasCode( $e, 'nosuchsection' ) );
189 }
190 }
191
192 /**
193 * Test action=edit&section=new
194 * Run it twice so we test adding a new section on a
195 * page that doesn't exist (T54830) and one that
196 * does exist
197 */
198 public function testEditNewSection() {
199 $name = 'Help:ApiEditPageTest_testEditNewSection';
200
201 // Test on a page that does not already exist
202 $this->assertFalse( Title::newFromText( $name )->exists() );
203 list( $re ) = $this->doApiRequestWithToken( [
204 'action' => 'edit',
205 'title' => $name,
206 'section' => 'new',
207 'text' => 'test',
208 'summary' => 'header',
209 ] );
210
211 $this->assertSame( 'Success', $re['edit']['result'] );
212 // Check the page text is correct
213 $text = WikiPage::factory( Title::newFromText( $name ) )
214 ->getContent( Revision::RAW )
215 ->getNativeData();
216 $this->assertSame( "== header ==\n\ntest", $text );
217
218 // Now on one that does
219 $this->assertTrue( Title::newFromText( $name )->exists() );
220 list( $re2 ) = $this->doApiRequestWithToken( [
221 'action' => 'edit',
222 'title' => $name,
223 'section' => 'new',
224 'text' => 'test',
225 'summary' => 'header',
226 ] );
227
228 $this->assertSame( 'Success', $re2['edit']['result'] );
229 $text = WikiPage::factory( Title::newFromText( $name ) )
230 ->getContent( Revision::RAW )
231 ->getNativeData();
232 $this->assertSame( "== header ==\n\ntest\n\n== header ==\n\ntest", $text );
233 }
234
235 /**
236 * Ensure we can edit through a redirect, if adding a section
237 */
238 public function testEdit_redirect() {
239 static $count = 0;
240 $count++;
241
242 // assume NS_HELP defaults to wikitext
243 $name = "Help:ApiEditPageTest_testEdit_redirect_$count";
244 $title = Title::newFromText( $name );
245 $page = WikiPage::factory( $title );
246
247 $rname = "Help:ApiEditPageTest_testEdit_redirect_r$count";
248 $rtitle = Title::newFromText( $rname );
249 $rpage = WikiPage::factory( $rtitle );
250
251 // base edit for content
252 $page->doEditContent( new WikitextContent( "Foo" ),
253 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
254 $this->forceRevisionDate( $page, '20120101000000' );
255 $baseTime = $page->getRevision()->getTimestamp();
256
257 // base edit for redirect
258 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
259 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
260 $this->forceRevisionDate( $rpage, '20120101000000' );
261
262 // conflicting edit to redirect
263 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
264 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
265 $this->forceRevisionDate( $rpage, '20120101020202' );
266
267 // try to save edit, following the redirect
268 list( $re, , ) = $this->doApiRequestWithToken( [
269 'action' => 'edit',
270 'title' => $rname,
271 'text' => 'nix bar!',
272 'basetimestamp' => $baseTime,
273 'section' => 'new',
274 'redirect' => true,
275 ] );
276
277 $this->assertSame( 'Success', $re['edit']['result'],
278 "no problems expected when following redirect" );
279 }
280
281 /**
282 * Ensure we cannot edit through a redirect, if attempting to overwrite content
283 */
284 public function testEdit_redirectText() {
285 static $count = 0;
286 $count++;
287
288 // assume NS_HELP defaults to wikitext
289 $name = "Help:ApiEditPageTest_testEdit_redirectText_$count";
290 $title = Title::newFromText( $name );
291 $page = WikiPage::factory( $title );
292
293 $rname = "Help:ApiEditPageTest_testEdit_redirectText_r$count";
294 $rtitle = Title::newFromText( $rname );
295 $rpage = WikiPage::factory( $rtitle );
296
297 // base edit for content
298 $page->doEditContent( new WikitextContent( "Foo" ),
299 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
300 $this->forceRevisionDate( $page, '20120101000000' );
301 $baseTime = $page->getRevision()->getTimestamp();
302
303 // base edit for redirect
304 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
305 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
306 $this->forceRevisionDate( $rpage, '20120101000000' );
307
308 // conflicting edit to redirect
309 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
310 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
311 $this->forceRevisionDate( $rpage, '20120101020202' );
312
313 // try to save edit, following the redirect but without creating a section
314 try {
315 $this->doApiRequestWithToken( [
316 'action' => 'edit',
317 'title' => $rname,
318 'text' => 'nix bar!',
319 'basetimestamp' => $baseTime,
320 'redirect' => true,
321 ] );
322
323 $this->fail( 'redirect-appendonly error expected' );
324 } catch ( ApiUsageException $ex ) {
325 $this->assertTrue( self::apiExceptionHasCode( $ex, 'redirect-appendonly' ) );
326 }
327 }
328
329 public function testEditConflict() {
330 static $count = 0;
331 $count++;
332
333 // assume NS_HELP defaults to wikitext
334 $name = "Help:ApiEditPageTest_testEditConflict_$count";
335 $title = Title::newFromText( $name );
336
337 $page = WikiPage::factory( $title );
338
339 // base edit
340 $page->doEditContent( new WikitextContent( "Foo" ),
341 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
342 $this->forceRevisionDate( $page, '20120101000000' );
343 $baseTime = $page->getRevision()->getTimestamp();
344
345 // conflicting edit
346 $page->doEditContent( new WikitextContent( "Foo bar" ),
347 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
348 $this->forceRevisionDate( $page, '20120101020202' );
349
350 // try to save edit, expect conflict
351 try {
352 $this->doApiRequestWithToken( [
353 'action' => 'edit',
354 'title' => $name,
355 'text' => 'nix bar!',
356 'basetimestamp' => $baseTime,
357 ] );
358
359 $this->fail( 'edit conflict expected' );
360 } catch ( ApiUsageException $ex ) {
361 $this->assertTrue( self::apiExceptionHasCode( $ex, 'editconflict' ) );
362 }
363 }
364
365 /**
366 * Ensure that editing using section=new will prevent simple conflicts
367 */
368 public function testEditConflict_newSection() {
369 static $count = 0;
370 $count++;
371
372 // assume NS_HELP defaults to wikitext
373 $name = "Help:ApiEditPageTest_testEditConflict_newSection_$count";
374 $title = Title::newFromText( $name );
375
376 $page = WikiPage::factory( $title );
377
378 // base edit
379 $page->doEditContent( new WikitextContent( "Foo" ),
380 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
381 $this->forceRevisionDate( $page, '20120101000000' );
382 $baseTime = $page->getRevision()->getTimestamp();
383
384 // conflicting edit
385 $page->doEditContent( new WikitextContent( "Foo bar" ),
386 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
387 $this->forceRevisionDate( $page, '20120101020202' );
388
389 // try to save edit, expect no conflict
390 list( $re, , ) = $this->doApiRequestWithToken( [
391 'action' => 'edit',
392 'title' => $name,
393 'text' => 'nix bar!',
394 'basetimestamp' => $baseTime,
395 'section' => 'new',
396 ] );
397
398 $this->assertSame( 'Success', $re['edit']['result'],
399 "no edit conflict expected here" );
400 }
401
402 public function testEditConflict_T43990() {
403 static $count = 0;
404 $count++;
405
406 /*
407 * T43990: if the target page has a newer revision than the redirect, then editing the
408 * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously
409 * caused an edit conflict to be detected.
410 */
411
412 // assume NS_HELP defaults to wikitext
413 $name = "Help:ApiEditPageTest_testEditConflict_redirect_T43990_$count";
414 $title = Title::newFromText( $name );
415 $page = WikiPage::factory( $title );
416
417 $rname = "Help:ApiEditPageTest_testEditConflict_redirect_T43990_r$count";
418 $rtitle = Title::newFromText( $rname );
419 $rpage = WikiPage::factory( $rtitle );
420
421 // base edit for content
422 $page->doEditContent( new WikitextContent( "Foo" ),
423 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
424 $this->forceRevisionDate( $page, '20120101000000' );
425
426 // base edit for redirect
427 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
428 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
429 $this->forceRevisionDate( $rpage, '20120101000000' );
430
431 // new edit to content
432 $page->doEditContent( new WikitextContent( "Foo bar" ),
433 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
434 $this->forceRevisionDate( $rpage, '20120101020202' );
435
436 // try to save edit; should work, following the redirect.
437 list( $re, , ) = $this->doApiRequestWithToken( [
438 'action' => 'edit',
439 'title' => $rname,
440 'text' => 'nix bar!',
441 'section' => 'new',
442 'redirect' => true,
443 ] );
444
445 $this->assertSame( 'Success', $re['edit']['result'],
446 "no edit conflict expected here" );
447 }
448
449 /**
450 * @param WikiPage $page
451 * @param string|int $timestamp
452 */
453 protected function forceRevisionDate( WikiPage $page, $timestamp ) {
454 $dbw = wfGetDB( DB_MASTER );
455
456 $dbw->update( 'revision',
457 [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
458 [ 'rev_id' => $page->getLatest() ] );
459
460 $page->clear();
461 }
462
463 public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
464 $this->setExpectedException(
465 ApiUsageException::class,
466 'Direct editing via API is not supported for content model ' .
467 'testing used by Dummy:ApiEditPageTest_nonTextPageEdit'
468 );
469
470 $this->doApiRequestWithToken( [
471 'action' => 'edit',
472 'title' => 'Dummy:ApiEditPageTest_nonTextPageEdit',
473 'text' => '{"animals":["kittens!"]}'
474 ] );
475 }
476
477 public function testSupportsDirectApiEditing_withContentHandlerOverride() {
478 $name = 'DummyNonText:ApiEditPageTest_testNonTextEdit';
479 $data = serialize( 'some bla bla text' );
480
481 $result = $this->doApiRequestWithToken( [
482 'action' => 'edit',
483 'title' => $name,
484 'text' => $data,
485 ] );
486
487 $apiResult = $result[0];
488
489 // Validate API result data
490 $this->assertArrayHasKey( 'edit', $apiResult );
491 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
492 $this->assertSame( 'Success', $apiResult['edit']['result'] );
493
494 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
495 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
496
497 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
498
499 // validate resulting revision
500 $page = WikiPage::factory( Title::newFromText( $name ) );
501 $this->assertSame( "testing-nontext", $page->getContentModel() );
502 $this->assertSame( $data, $page->getContent()->serialize() );
503 }
504
505 /**
506 * This test verifies that after changing the content model
507 * of a page, undoing that edit via the API will also
508 * undo the content model change.
509 */
510 public function testUndoAfterContentModelChange() {
511 $name = 'Help:' . __FUNCTION__;
512 $uploader = self::$users['uploader']->getUser();
513 $sysop = self::$users['sysop']->getUser();
514
515 $apiResult = $this->doApiRequestWithToken( [
516 'action' => 'edit',
517 'title' => $name,
518 'text' => 'some text',
519 ], null, $sysop )[0];
520
521 // Check success
522 $this->assertArrayHasKey( 'edit', $apiResult );
523 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
524 $this->assertSame( 'Success', $apiResult['edit']['result'] );
525 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
526 // Content model is wikitext
527 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
528
529 // Convert the page to JSON
530 $apiResult = $this->doApiRequestWithToken( [
531 'action' => 'edit',
532 'title' => $name,
533 'text' => '{}',
534 'contentmodel' => 'json',
535 ], null, $uploader )[0];
536
537 // Check success
538 $this->assertArrayHasKey( 'edit', $apiResult );
539 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
540 $this->assertSame( 'Success', $apiResult['edit']['result'] );
541 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
542 $this->assertSame( 'json', $apiResult['edit']['contentmodel'] );
543
544 $apiResult = $this->doApiRequestWithToken( [
545 'action' => 'edit',
546 'title' => $name,
547 'undo' => $apiResult['edit']['newrevid']
548 ], null, $sysop )[0];
549
550 // Check success
551 $this->assertArrayHasKey( 'edit', $apiResult );
552 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
553 $this->assertSame( 'Success', $apiResult['edit']['result'] );
554 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
555 // Check that the contentmodel is back to wikitext now.
556 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
557 }
558
559 // The tests below are mostly not commented because they do exactly what
560 // you'd expect from the name.
561
562 public function testCorrectContentFormat() {
563 $name = 'Help:' . ucfirst( __FUNCTION__ );
564
565 $this->doApiRequestWithToken( [
566 'action' => 'edit',
567 'title' => $name,
568 'text' => 'some text',
569 'contentmodel' => 'wikitext',
570 'contentformat' => 'text/x-wiki',
571 ] );
572
573 $this->assertTrue( Title::newFromText( $name )->exists() );
574 }
575
576 public function testUnsupportedContentFormat() {
577 $name = 'Help:' . ucfirst( __FUNCTION__ );
578
579 $this->setExpectedException( ApiUsageException::class,
580 'Unrecognized value for parameter "contentformat": nonexistent format.' );
581
582 try {
583 $this->doApiRequestWithToken( [
584 'action' => 'edit',
585 'title' => $name,
586 'text' => 'some text',
587 'contentformat' => 'nonexistent format',
588 ] );
589 } finally {
590 $this->assertFalse( Title::newFromText( $name )->exists() );
591 }
592 }
593
594 public function testMismatchedContentFormat() {
595 $name = 'Help:' . ucfirst( __FUNCTION__ );
596
597 $this->setExpectedException( ApiUsageException::class,
598 'The requested format text/plain is not supported for content ' .
599 "model wikitext used by $name." );
600
601 try {
602 $this->doApiRequestWithToken( [
603 'action' => 'edit',
604 'title' => $name,
605 'text' => 'some text',
606 'contentmodel' => 'wikitext',
607 'contentformat' => 'text/plain',
608 ] );
609 } finally {
610 $this->assertFalse( Title::newFromText( $name )->exists() );
611 }
612 }
613
614 public function testUndoToInvalidRev() {
615 $name = 'Help:' . ucfirst( __FUNCTION__ );
616
617 $revId = $this->editPage( $name, 'Some text' )->value['revision']
618 ->getId();
619 $revId++;
620
621 $this->setExpectedException( ApiUsageException::class,
622 "There is no revision with ID $revId." );
623
624 $this->doApiRequestWithToken( [
625 'action' => 'edit',
626 'title' => $name,
627 'undo' => $revId,
628 ] );
629 }
630
631 /**
632 * Tests what happens if the undo parameter is a valid revision, but
633 * the undoafter parameter doesn't refer to a revision that exists in the
634 * database.
635 */
636 public function testUndoAfterToInvalidRev() {
637 // We can't just pick a large number for undoafter (as in
638 // testUndoToInvalidRev above), because then MediaWiki will helpfully
639 // assume we switched around undo and undoafter and we'll test the code
640 // path for undo being invalid, not undoafter. So instead we delete
641 // the revision from the database. In real life this case could come
642 // up if a revision number was skipped, e.g., if two transactions try
643 // to insert new revision rows at once and the first one to succeed
644 // gets rolled back.
645 $name = 'Help:' . ucfirst( __FUNCTION__ );
646 $titleObj = Title::newFromText( $name );
647
648 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
649 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
650 $revId3 = $this->editPage( $name, '3' )->value['revision']->getId();
651
652 // Make the middle revision disappear
653 $dbw = wfGetDB( DB_MASTER );
654 $dbw->delete( 'revision', [ 'rev_id' => $revId2 ], __METHOD__ );
655 $dbw->update( 'revision', [ 'rev_parent_id' => $revId1 ],
656 [ 'rev_id' => $revId3 ], __METHOD__ );
657
658 $this->setExpectedException( ApiUsageException::class,
659 "There is no revision with ID $revId2." );
660
661 $this->doApiRequestWithToken( [
662 'action' => 'edit',
663 'title' => $name,
664 'undo' => $revId3,
665 'undoafter' => $revId2,
666 ] );
667 }
668
669 /**
670 * Tests what happens if the undo parameter is a valid revision, but
671 * undoafter is hidden (rev_deleted).
672 */
673 public function testUndoAfterToHiddenRev() {
674 $name = 'Help:' . ucfirst( __FUNCTION__ );
675 $titleObj = Title::newFromText( $name );
676
677 $this->editPage( $name, '0' );
678
679 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
680
681 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
682
683 // Hide the middle revision
684 $list = RevisionDeleter::createList( 'revision',
685 RequestContext::getMain(), $titleObj, [ $revId1 ] );
686 $list->setVisibility( [
687 'value' => [ Revision::DELETED_TEXT => 1 ],
688 'comment' => 'Bye-bye',
689 ] );
690
691 $this->setExpectedException( ApiUsageException::class,
692 "There is no revision with ID $revId1." );
693
694 $this->doApiRequestWithToken( [
695 'action' => 'edit',
696 'title' => $name,
697 'undo' => $revId2,
698 'undoafter' => $revId1,
699 ] );
700 }
701
702 /**
703 * Test undo when a revision with a higher id has an earlier timestamp.
704 * This can happen if importing an old revision.
705 */
706 public function testUndoWithSwappedRevisions() {
707 $name = 'Help:' . ucfirst( __FUNCTION__ );
708 $titleObj = Title::newFromText( $name );
709
710 $this->editPage( $name, '0' );
711
712 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
713
714 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
715
716 // Now monkey with the timestamp
717 $dbw = wfGetDB( DB_MASTER );
718 $dbw->update(
719 'revision',
720 [ 'rev_timestamp' => $dbw->timestamp( time() - 86400 ) ],
721 [ 'rev_id' => $revId1 ],
722 __METHOD__
723 );
724
725 $this->doApiRequestWithToken( [
726 'action' => 'edit',
727 'title' => $name,
728 'undo' => $revId2,
729 'undoafter' => $revId1,
730 ] );
731
732 $text = ( new WikiPage( $titleObj ) )->getContent()->getNativeData();
733
734 // This is wrong! It should be 1. But let's test for our incorrect
735 // behavior for now, so if someone fixes it they'll fix the test as
736 // well to expect 1. If we disabled the test, it might stay disabled
737 // even once the bug is fixed, which would be a shame.
738 $this->assertSame( '2', $text );
739 }
740
741 public function testUndoWithConflicts() {
742 $name = 'Help:' . ucfirst( __FUNCTION__ );
743
744 $this->setExpectedException( ApiUsageException::class,
745 'The edit could not be undone due to conflicting intermediate edits.' );
746
747 $this->editPage( $name, '1' );
748
749 $revId = $this->editPage( $name, '2' )->value['revision']->getId();
750
751 $this->editPage( $name, '3' );
752
753 $this->doApiRequestWithToken( [
754 'action' => 'edit',
755 'title' => $name,
756 'undo' => $revId,
757 ] );
758
759 $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
760 ->getNativeData();
761 $this->assertSame( '3', $text );
762 }
763
764 /**
765 * undoafter is supposed to be less than undo. If not, we reverse their
766 * meaning, so that the two are effectively interchangeable.
767 */
768 public function testReversedUndoAfter() {
769 $name = 'Help:' . ucfirst( __FUNCTION__ );
770
771 $this->editPage( $name, '0' );
772 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
773 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
774
775 $this->doApiRequestWithToken( [
776 'action' => 'edit',
777 'title' => $name,
778 'undo' => $revId1,
779 'undoafter' => $revId2,
780 ] );
781
782 $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
783 ->getNativeData();
784 $this->assertSame( '1', $text );
785 }
786
787 public function testUndoToRevFromDifferentPage() {
788 $name = 'Help:' . ucfirst( __FUNCTION__ );
789
790 $this->editPage( "$name-1", 'Some text' );
791 $revId = $this->editPage( "$name-1", 'Some more text' )
792 ->value['revision']->getId();
793
794 $this->editPage( "$name-2", 'Some text' );
795
796 $this->setExpectedException( ApiUsageException::class,
797 "r$revId is not a revision of $name-2." );
798
799 $this->doApiRequestWithToken( [
800 'action' => 'edit',
801 'title' => "$name-2",
802 'undo' => $revId,
803 ] );
804 }
805
806 public function testUndoAfterToRevFromDifferentPage() {
807 $name = 'Help:' . ucfirst( __FUNCTION__ );
808
809 $revId1 = $this->editPage( "$name-1", 'Some text' )
810 ->value['revision']->getId();
811
812 $revId2 = $this->editPage( "$name-2", 'Some text' )
813 ->value['revision']->getId();
814
815 $this->setExpectedException( ApiUsageException::class,
816 "r$revId1 is not a revision of $name-2." );
817
818 $this->doApiRequestWithToken( [
819 'action' => 'edit',
820 'title' => "$name-2",
821 'undo' => $revId2,
822 'undoafter' => $revId1,
823 ] );
824 }
825
826 public function testMd5Text() {
827 $name = 'Help:' . ucfirst( __FUNCTION__ );
828
829 $this->assertFalse( Title::newFromText( $name )->exists() );
830
831 $this->doApiRequestWithToken( [
832 'action' => 'edit',
833 'title' => $name,
834 'text' => 'Some text',
835 'md5' => md5( 'Some text' ),
836 ] );
837
838 $this->assertTrue( Title::newFromText( $name )->exists() );
839 }
840
841 public function testMd5PrependText() {
842 $name = 'Help:' . ucfirst( __FUNCTION__ );
843
844 $this->editPage( $name, 'Some text' );
845
846 $this->doApiRequestWithToken( [
847 'action' => 'edit',
848 'title' => $name,
849 'prependtext' => 'Alert: ',
850 'md5' => md5( 'Alert: ' ),
851 ] );
852
853 $text = ( new WikiPage( Title::newFromText( $name ) ) )
854 ->getContent()->getNativeData();
855 $this->assertSame( 'Alert: Some text', $text );
856 }
857
858 public function testMd5AppendText() {
859 $name = 'Help:' . ucfirst( __FUNCTION__ );
860
861 $this->editPage( $name, 'Some text' );
862
863 $this->doApiRequestWithToken( [
864 'action' => 'edit',
865 'title' => $name,
866 'appendtext' => ' is nice',
867 'md5' => md5( ' is nice' ),
868 ] );
869
870 $text = ( new WikiPage( Title::newFromText( $name ) ) )
871 ->getContent()->getNativeData();
872 $this->assertSame( 'Some text is nice', $text );
873 }
874
875 public function testMd5PrependAndAppendText() {
876 $name = 'Help:' . ucfirst( __FUNCTION__ );
877
878 $this->editPage( $name, 'Some text' );
879
880 $this->doApiRequestWithToken( [
881 'action' => 'edit',
882 'title' => $name,
883 'prependtext' => 'Alert: ',
884 'appendtext' => ' is nice',
885 'md5' => md5( 'Alert: is nice' ),
886 ] );
887
888 $text = ( new WikiPage( Title::newFromText( $name ) ) )
889 ->getContent()->getNativeData();
890 $this->assertSame( 'Alert: Some text is nice', $text );
891 }
892
893 public function testIncorrectMd5Text() {
894 $name = 'Help:' . ucfirst( __FUNCTION__ );
895
896 $this->setExpectedException( ApiUsageException::class,
897 'The supplied MD5 hash was incorrect.' );
898
899 $this->doApiRequestWithToken( [
900 'action' => 'edit',
901 'title' => $name,
902 'text' => 'Some text',
903 'md5' => md5( '' ),
904 ] );
905 }
906
907 public function testIncorrectMd5PrependText() {
908 $name = 'Help:' . ucfirst( __FUNCTION__ );
909
910 $this->setExpectedException( ApiUsageException::class,
911 'The supplied MD5 hash was incorrect.' );
912
913 $this->doApiRequestWithToken( [
914 'action' => 'edit',
915 'title' => $name,
916 'prependtext' => 'Some ',
917 'appendtext' => 'text',
918 'md5' => md5( 'Some ' ),
919 ] );
920 }
921
922 public function testIncorrectMd5AppendText() {
923 $name = 'Help:' . ucfirst( __FUNCTION__ );
924
925 $this->setExpectedException( ApiUsageException::class,
926 'The supplied MD5 hash was incorrect.' );
927
928 $this->doApiRequestWithToken( [
929 'action' => 'edit',
930 'title' => $name,
931 'prependtext' => 'Some ',
932 'appendtext' => 'text',
933 'md5' => md5( 'text' ),
934 ] );
935 }
936
937 public function testCreateOnly() {
938 $name = 'Help:' . ucfirst( __FUNCTION__ );
939
940 $this->setExpectedException( ApiUsageException::class,
941 'The article you tried to create has been created already.' );
942
943 $this->editPage( $name, 'Some text' );
944 $this->assertTrue( Title::newFromText( $name )->exists() );
945
946 try {
947 $this->doApiRequestWithToken( [
948 'action' => 'edit',
949 'title' => $name,
950 'text' => 'Some more text',
951 'createonly' => '',
952 ] );
953 } finally {
954 // Validate that content was not changed
955 $text = ( new WikiPage( Title::newFromText( $name ) ) )
956 ->getContent()->getNativeData();
957
958 $this->assertSame( 'Some text', $text );
959 }
960 }
961
962 public function testNoCreate() {
963 $name = 'Help:' . ucfirst( __FUNCTION__ );
964
965 $this->setExpectedException( ApiUsageException::class,
966 "The page you specified doesn't exist." );
967
968 $this->assertFalse( Title::newFromText( $name )->exists() );
969
970 try {
971 $this->doApiRequestWithToken( [
972 'action' => 'edit',
973 'title' => $name,
974 'text' => 'Some text',
975 'nocreate' => '',
976 ] );
977 } finally {
978 $this->assertFalse( Title::newFromText( $name )->exists() );
979 }
980 }
981
982 /**
983 * Appending/prepending is currently only supported for TextContent. We
984 * test this right now, and when support is added this test should be
985 * replaced by tests that the support is correct.
986 */
987 public function testAppendWithNonTextContentHandler() {
988 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
989
990 $this->setExpectedException( ApiUsageException::class,
991 "Can't append to pages using content model testing-nontext." );
992
993 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
994 function ( Title $title, &$model ) use ( $name ) {
995 if ( $title->getPrefixedText() === $name ) {
996 $model = 'testing-nontext';
997 }
998 return true;
999 }
1000 );
1001
1002 $this->doApiRequestWithToken( [
1003 'action' => 'edit',
1004 'title' => $name,
1005 'appendtext' => 'Some text',
1006 ] );
1007 }
1008
1009 public function testAppendInMediaWikiNamespace() {
1010 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1011
1012 $this->assertFalse( Title::newFromText( $name )->exists() );
1013
1014 $this->doApiRequestWithToken( [
1015 'action' => 'edit',
1016 'title' => $name,
1017 'appendtext' => 'Some text',
1018 ] );
1019
1020 $this->assertTrue( Title::newFromText( $name )->exists() );
1021 }
1022
1023 public function testAppendInMediaWikiNamespaceWithSerializationError() {
1024 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1025
1026 $this->setExpectedException( ApiUsageException::class,
1027 'Content serialization failed: Could not unserialize content' );
1028
1029 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1030 function ( Title $title, &$model ) use ( $name ) {
1031 if ( $title->getPrefixedText() === $name ) {
1032 $model = 'testing-serialize-error';
1033 }
1034 return true;
1035 }
1036 );
1037
1038 $this->doApiRequestWithToken( [
1039 'action' => 'edit',
1040 'title' => $name,
1041 'appendtext' => 'Some text',
1042 ] );
1043 }
1044
1045 public function testAppendNewSection() {
1046 $name = 'Help:' . ucfirst( __FUNCTION__ );
1047
1048 $this->editPage( $name, 'Initial content' );
1049
1050 $this->doApiRequestWithToken( [
1051 'action' => 'edit',
1052 'title' => $name,
1053 'appendtext' => '== New section ==',
1054 'section' => 'new',
1055 ] );
1056
1057 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1058 ->getContent()->getNativeData();
1059
1060 $this->assertSame( "Initial content\n\n== New section ==", $text );
1061 }
1062
1063 public function testAppendNewSectionWithInvalidContentModel() {
1064 $name = 'Help:' . ucfirst( __FUNCTION__ );
1065
1066 $this->setExpectedException( ApiUsageException::class,
1067 'Sections are not supported for content model text.' );
1068
1069 $this->editPage( $name, 'Initial content' );
1070
1071 $this->doApiRequestWithToken( [
1072 'action' => 'edit',
1073 'title' => $name,
1074 'appendtext' => '== New section ==',
1075 'section' => 'new',
1076 'contentmodel' => 'text',
1077 ] );
1078 }
1079
1080 public function testAppendNewSectionWithTitle() {
1081 $name = 'Help:' . ucfirst( __FUNCTION__ );
1082
1083 $this->editPage( $name, 'Initial content' );
1084
1085 $this->doApiRequestWithToken( [
1086 'action' => 'edit',
1087 'title' => $name,
1088 'sectiontitle' => 'My section',
1089 'appendtext' => 'More content',
1090 'section' => 'new',
1091 ] );
1092
1093 $page = new WikiPage( Title::newFromText( $name ) );
1094
1095 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1096 $page->getContent()->getNativeData() );
1097 $this->assertSame( '/* My section */ new section',
1098 $page->getRevision()->getComment() );
1099 }
1100
1101 public function testAppendNewSectionWithSummary() {
1102 $name = 'Help:' . ucfirst( __FUNCTION__ );
1103
1104 $this->editPage( $name, 'Initial content' );
1105
1106 $this->doApiRequestWithToken( [
1107 'action' => 'edit',
1108 'title' => $name,
1109 'appendtext' => 'More content',
1110 'section' => 'new',
1111 'summary' => 'Add new section',
1112 ] );
1113
1114 $page = new WikiPage( Title::newFromText( $name ) );
1115
1116 $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content",
1117 $page->getContent()->getNativeData() );
1118 // EditPage actually assumes the summary is the section name here
1119 $this->assertSame( '/* Add new section */ new section',
1120 $page->getRevision()->getComment() );
1121 }
1122
1123 public function testAppendNewSectionWithTitleAndSummary() {
1124 $name = 'Help:' . ucfirst( __FUNCTION__ );
1125
1126 $this->editPage( $name, 'Initial content' );
1127
1128 $this->doApiRequestWithToken( [
1129 'action' => 'edit',
1130 'title' => $name,
1131 'sectiontitle' => 'My section',
1132 'appendtext' => 'More content',
1133 'section' => 'new',
1134 'summary' => 'Add new section',
1135 ] );
1136
1137 $page = new WikiPage( Title::newFromText( $name ) );
1138
1139 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1140 $page->getContent()->getNativeData() );
1141 $this->assertSame( 'Add new section',
1142 $page->getRevision()->getComment() );
1143 }
1144
1145 public function testAppendToSection() {
1146 $name = 'Help:' . ucfirst( __FUNCTION__ );
1147
1148 $this->editPage( $name, "== Section 1 ==\n\nContent\n\n" .
1149 "== Section 2 ==\n\nFascinating!" );
1150
1151 $this->doApiRequestWithToken( [
1152 'action' => 'edit',
1153 'title' => $name,
1154 'appendtext' => ' and more content',
1155 'section' => '1',
1156 ] );
1157
1158 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1159 ->getContent()->getNativeData();
1160
1161 $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" .
1162 "== Section 2 ==\n\nFascinating!", $text );
1163 }
1164
1165 public function testAppendToFirstSection() {
1166 $name = 'Help:' . ucfirst( __FUNCTION__ );
1167
1168 $this->editPage( $name, "Content\n\n== Section 1 ==\n\nFascinating!" );
1169
1170 $this->doApiRequestWithToken( [
1171 'action' => 'edit',
1172 'title' => $name,
1173 'appendtext' => ' and more content',
1174 'section' => '0',
1175 ] );
1176
1177 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1178 ->getContent()->getNativeData();
1179
1180 $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" .
1181 "Fascinating!", $text );
1182 }
1183
1184 public function testAppendToNonexistentSection() {
1185 $name = 'Help:' . ucfirst( __FUNCTION__ );
1186
1187 $this->setExpectedException( ApiUsageException::class, 'There is no section 1.' );
1188
1189 $this->editPage( $name, 'Content' );
1190
1191 try {
1192 $this->doApiRequestWithToken( [
1193 'action' => 'edit',
1194 'title' => $name,
1195 'appendtext' => ' and more content',
1196 'section' => '1',
1197 ] );
1198 } finally {
1199 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1200 ->getContent()->getNativeData();
1201
1202 $this->assertSame( 'Content', $text );
1203 }
1204 }
1205
1206 public function testEditMalformedSection() {
1207 $name = 'Help:' . ucfirst( __FUNCTION__ );
1208
1209 $this->setExpectedException( ApiUsageException::class,
1210 'The "section" parameter must be a valid section ID or "new".' );
1211 $this->editPage( $name, 'Content' );
1212
1213 try {
1214 $this->doApiRequestWithToken( [
1215 'action' => 'edit',
1216 'title' => $name,
1217 'text' => 'Different content',
1218 'section' => 'It is unlikely that this is valid',
1219 ] );
1220 } finally {
1221 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1222 ->getContent()->getNativeData();
1223
1224 $this->assertSame( 'Content', $text );
1225 }
1226 }
1227
1228 public function testEditWithStartTimestamp() {
1229 $name = 'Help:' . ucfirst( __FUNCTION__ );
1230 $this->setExpectedException( ApiUsageException::class,
1231 'The page has been deleted since you fetched its timestamp.' );
1232
1233 $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
1234
1235 $this->editPage( $name, 'Some text' );
1236
1237 $pageObj = new WikiPage( Title::newFromText( $name ) );
1238 $pageObj->doDeleteArticle( 'Bye-bye' );
1239
1240 $this->assertFalse( $pageObj->exists() );
1241
1242 try {
1243 $this->doApiRequestWithToken( [
1244 'action' => 'edit',
1245 'title' => $name,
1246 'text' => 'Different text',
1247 'starttimestamp' => $startTime,
1248 ] );
1249 } finally {
1250 $this->assertFalse( $pageObj->exists() );
1251 }
1252 }
1253
1254 public function testEditMinor() {
1255 $name = 'Help:' . ucfirst( __FUNCTION__ );
1256
1257 $this->editPage( $name, 'Some text' );
1258
1259 $this->doApiRequestWithToken( [
1260 'action' => 'edit',
1261 'title' => $name,
1262 'text' => 'Different text',
1263 'minor' => '',
1264 ] );
1265
1266 $revisionStore = \MediaWiki\MediaWikiServices::getInstance()->getRevisionStore();
1267 $revision = $revisionStore->getRevisionByTitle( Title::newFromText( $name ) );
1268 $this->assertTrue( $revision->isMinor() );
1269 }
1270
1271 public function testEditRecreate() {
1272 $name = 'Help:' . ucfirst( __FUNCTION__ );
1273
1274 $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
1275
1276 $this->editPage( $name, 'Some text' );
1277
1278 $pageObj = new WikiPage( Title::newFromText( $name ) );
1279 $pageObj->doDeleteArticle( 'Bye-bye' );
1280
1281 $this->assertFalse( $pageObj->exists() );
1282
1283 $this->doApiRequestWithToken( [
1284 'action' => 'edit',
1285 'title' => $name,
1286 'text' => 'Different text',
1287 'starttimestamp' => $startTime,
1288 'recreate' => '',
1289 ] );
1290
1291 $this->assertTrue( Title::newFromText( $name )->exists() );
1292 }
1293
1294 public function testEditWatch() {
1295 $name = 'Help:' . ucfirst( __FUNCTION__ );
1296 $user = self::$users['sysop']->getUser();
1297
1298 $this->doApiRequestWithToken( [
1299 'action' => 'edit',
1300 'title' => $name,
1301 'text' => 'Some text',
1302 'watch' => '',
1303 ] );
1304
1305 $this->assertTrue( Title::newFromText( $name )->exists() );
1306 $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
1307 }
1308
1309 public function testEditUnwatch() {
1310 $name = 'Help:' . ucfirst( __FUNCTION__ );
1311 $user = self::$users['sysop']->getUser();
1312 $titleObj = Title::newFromText( $name );
1313
1314 $user->addWatch( $titleObj );
1315
1316 $this->assertFalse( $titleObj->exists() );
1317 $this->assertTrue( $user->isWatched( $titleObj ) );
1318
1319 $this->doApiRequestWithToken( [
1320 'action' => 'edit',
1321 'title' => $name,
1322 'text' => 'Some text',
1323 'unwatch' => '',
1324 ] );
1325
1326 $this->assertTrue( $titleObj->exists() );
1327 $this->assertFalse( $user->isWatched( $titleObj ) );
1328 }
1329
1330 public function testEditWithTag() {
1331 $name = 'Help:' . ucfirst( __FUNCTION__ );
1332
1333 ChangeTags::defineTag( 'custom tag' );
1334
1335 $revId = $this->doApiRequestWithToken( [
1336 'action' => 'edit',
1337 'title' => $name,
1338 'text' => 'Some text',
1339 'tags' => 'custom tag',
1340 ] )[0]['edit']['newrevid'];
1341
1342 $dbw = wfGetDB( DB_MASTER );
1343 $this->assertSame( 'custom tag', $dbw->selectField(
1344 'change_tag', 'ct_tag', [ 'ct_rev_id' => $revId ], __METHOD__ ) );
1345 }
1346
1347 public function testEditWithoutTagPermission() {
1348 $name = 'Help:' . ucfirst( __FUNCTION__ );
1349
1350 $this->setExpectedException( ApiUsageException::class,
1351 'You do not have permission to apply change tags along with your changes.' );
1352
1353 $this->assertFalse( Title::newFromText( $name )->exists() );
1354
1355 ChangeTags::defineTag( 'custom tag' );
1356 $this->setMwGlobals( 'wgRevokePermissions',
1357 [ 'user' => [ 'applychangetags' => true ] ] );
1358 try {
1359 $this->doApiRequestWithToken( [
1360 'action' => 'edit',
1361 'title' => $name,
1362 'text' => 'Some text',
1363 'tags' => 'custom tag',
1364 ] );
1365 } finally {
1366 $this->assertFalse( Title::newFromText( $name )->exists() );
1367 }
1368 }
1369
1370 public function testEditAbortedByHook() {
1371 $name = 'Help:' . ucfirst( __FUNCTION__ );
1372
1373 $this->setExpectedException( ApiUsageException::class,
1374 'The modification you tried to make was aborted by an extension.' );
1375
1376 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1377 'hook-APIEditBeforeSave-closure)' );
1378
1379 $this->setTemporaryHook( 'APIEditBeforeSave',
1380 function () {
1381 return false;
1382 }
1383 );
1384
1385 try {
1386 $this->doApiRequestWithToken( [
1387 'action' => 'edit',
1388 'title' => $name,
1389 'text' => 'Some text',
1390 ] );
1391 } finally {
1392 $this->assertFalse( Title::newFromText( $name )->exists() );
1393 }
1394 }
1395
1396 public function testEditAbortedByHookWithCustomOutput() {
1397 $name = 'Help:' . ucfirst( __FUNCTION__ );
1398
1399 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1400 'hook-APIEditBeforeSave-closure)' );
1401
1402 $this->setTemporaryHook( 'APIEditBeforeSave',
1403 function ( $unused1, $unused2, &$r ) {
1404 $r['msg'] = 'Some message';
1405 return false;
1406 } );
1407
1408 $result = $this->doApiRequestWithToken( [
1409 'action' => 'edit',
1410 'title' => $name,
1411 'text' => 'Some text',
1412 ] );
1413 Wikimedia\restoreWarnings();
1414
1415 $this->assertSame( [ 'msg' => 'Some message', 'result' => 'Failure' ],
1416 $result[0]['edit'] );
1417
1418 $this->assertFalse( Title::newFromText( $name )->exists() );
1419 }
1420
1421 public function testEditAbortedByEditPageHookWithResult() {
1422 $name = 'Help:' . ucfirst( __FUNCTION__ );
1423
1424 $this->setTemporaryHook( 'EditFilterMergedContent',
1425 function ( $unused1, $unused2, Status $status ) {
1426 $status->apiHookResult = [ 'msg' => 'A message for you!' ];
1427 return false;
1428 } );
1429
1430 $res = $this->doApiRequestWithToken( [
1431 'action' => 'edit',
1432 'title' => $name,
1433 'text' => 'Some text',
1434 ] );
1435
1436 $this->assertFalse( Title::newFromText( $name )->exists() );
1437 $this->assertSame( [ 'edit' => [ 'msg' => 'A message for you!',
1438 'result' => 'Failure' ] ], $res[0] );
1439 }
1440
1441 public function testEditAbortedByEditPageHookWithNoResult() {
1442 $name = 'Help:' . ucfirst( __FUNCTION__ );
1443
1444 $this->setExpectedException( ApiUsageException::class,
1445 'The modification you tried to make was aborted by an extension.' );
1446
1447 $this->setTemporaryHook( 'EditFilterMergedContent',
1448 function () {
1449 return false;
1450 }
1451 );
1452
1453 try {
1454 $this->doApiRequestWithToken( [
1455 'action' => 'edit',
1456 'title' => $name,
1457 'text' => 'Some text',
1458 ] );
1459 } finally {
1460 $this->assertFalse( Title::newFromText( $name )->exists() );
1461 }
1462 }
1463
1464 public function testEditWhileBlocked() {
1465 $name = 'Help:' . ucfirst( __FUNCTION__ );
1466
1467 $this->setExpectedException( ApiUsageException::class,
1468 'You have been blocked from editing.' );
1469
1470 $block = new Block( [
1471 'address' => self::$users['sysop']->getUser()->getName(),
1472 'by' => self::$users['sysop']->getUser()->getId(),
1473 'reason' => 'Capriciousness',
1474 'timestamp' => '19370101000000',
1475 'expiry' => 'infinity',
1476 ] );
1477 $block->insert();
1478
1479 try {
1480 $this->doApiRequestWithToken( [
1481 'action' => 'edit',
1482 'title' => $name,
1483 'text' => 'Some text',
1484 ] );
1485 } finally {
1486 $block->delete();
1487 self::$users['sysop']->getUser()->clearInstanceCache();
1488 }
1489 }
1490
1491 public function testEditWhileReadOnly() {
1492 $name = 'Help:' . ucfirst( __FUNCTION__ );
1493
1494 $this->setExpectedException( ApiUsageException::class,
1495 'The wiki is currently in read-only mode.' );
1496
1497 $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1498 $svc->setReason( "Read-only for testing" );
1499
1500 try {
1501 $this->doApiRequestWithToken( [
1502 'action' => 'edit',
1503 'title' => $name,
1504 'text' => 'Some text',
1505 ] );
1506 } finally {
1507 $svc->setReason( false );
1508 }
1509 }
1510
1511 public function testCreateImageRedirectAnon() {
1512 $name = 'File:' . ucfirst( __FUNCTION__ );
1513
1514 $this->setExpectedException( ApiUsageException::class,
1515 "Anonymous users can't create image redirects." );
1516
1517 $this->doApiRequestWithToken( [
1518 'action' => 'edit',
1519 'title' => $name,
1520 'text' => '#REDIRECT [[File:Other file.png]]',
1521 ], null, new User() );
1522 }
1523
1524 public function testCreateImageRedirectLoggedIn() {
1525 $name = 'File:' . ucfirst( __FUNCTION__ );
1526
1527 $this->setExpectedException( ApiUsageException::class,
1528 "You don't have permission to create image redirects." );
1529
1530 $this->setMwGlobals( 'wgRevokePermissions',
1531 [ 'user' => [ 'upload' => true ] ] );
1532
1533 $this->doApiRequestWithToken( [
1534 'action' => 'edit',
1535 'title' => $name,
1536 'text' => '#REDIRECT [[File:Other file.png]]',
1537 ] );
1538 }
1539
1540 public function testTooBigEdit() {
1541 $name = 'Help:' . ucfirst( __FUNCTION__ );
1542
1543 $this->setExpectedException( ApiUsageException::class,
1544 'The content you supplied exceeds the article size limit of 1 kilobyte.' );
1545
1546 $this->setMwGlobals( 'wgMaxArticleSize', 1 );
1547
1548 $text = str_repeat( '!', 1025 );
1549
1550 $this->doApiRequestWithToken( [
1551 'action' => 'edit',
1552 'title' => $name,
1553 'text' => $text,
1554 ] );
1555 }
1556
1557 public function testProhibitedAnonymousEdit() {
1558 $name = 'Help:' . ucfirst( __FUNCTION__ );
1559
1560 $this->setExpectedException( ApiUsageException::class,
1561 'The action you have requested is limited to users in the group: ' );
1562
1563 $this->setMwGlobals( 'wgRevokePermissions', [ '*' => [ 'edit' => true ] ] );
1564
1565 $this->doApiRequestWithToken( [
1566 'action' => 'edit',
1567 'title' => $name,
1568 'text' => 'Some text',
1569 ], null, new User() );
1570 }
1571
1572 public function testProhibitedChangeContentModel() {
1573 $name = 'Help:' . ucfirst( __FUNCTION__ );
1574
1575 $this->setExpectedException( ApiUsageException::class,
1576 "You don't have permission to change the content model of a page." );
1577
1578 $this->setMwGlobals( 'wgRevokePermissions',
1579 [ 'user' => [ 'editcontentmodel' => true ] ] );
1580
1581 $this->doApiRequestWithToken( [
1582 'action' => 'edit',
1583 'title' => $name,
1584 'text' => 'Some text',
1585 'contentmodel' => 'json',
1586 ] );
1587 }
1588 }