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