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