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