4790f6b7271b8a52fbb92e693fc3baf351dfe313
[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 ] );
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 ] );
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 ] );
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 ] );
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 ] );
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
533 $apiResult = $this->doApiRequestWithToken( [
534 'action' => 'edit',
535 'title' => $name,
536 'text' => 'some text',
537 ], null, $sysop )[0];
538
539 // Check success
540 $this->assertArrayHasKey( 'edit', $apiResult );
541 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
542 $this->assertSame( 'Success', $apiResult['edit']['result'] );
543 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
544 // Content model is wikitext
545 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
546
547 // Convert the page to JSON
548 $apiResult = $this->doApiRequestWithToken( [
549 'action' => 'edit',
550 'title' => $name,
551 'text' => '{}',
552 'contentmodel' => 'json',
553 ], null, $uploader )[0];
554
555 // Check success
556 $this->assertArrayHasKey( 'edit', $apiResult );
557 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
558 $this->assertSame( 'Success', $apiResult['edit']['result'] );
559 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
560 $this->assertSame( 'json', $apiResult['edit']['contentmodel'] );
561
562 $apiResult = $this->doApiRequestWithToken( [
563 'action' => 'edit',
564 'title' => $name,
565 'undo' => $apiResult['edit']['newrevid']
566 ], null, $sysop )[0];
567
568 // Check success
569 $this->assertArrayHasKey( 'edit', $apiResult );
570 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
571 $this->assertSame( 'Success', $apiResult['edit']['result'] );
572 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
573 // Check that the contentmodel is back to wikitext now.
574 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
575 }
576
577 // The tests below are mostly not commented because they do exactly what
578 // you'd expect from the name.
579
580 public function testCorrectContentFormat() {
581 $name = 'Help:' . ucfirst( __FUNCTION__ );
582
583 $this->doApiRequestWithToken( [
584 'action' => 'edit',
585 'title' => $name,
586 'text' => 'some text',
587 'contentmodel' => 'wikitext',
588 'contentformat' => 'text/x-wiki',
589 ] );
590
591 $this->assertTrue( Title::newFromText( $name )->exists() );
592 }
593
594 public function testUnsupportedContentFormat() {
595 $name = 'Help:' . ucfirst( __FUNCTION__ );
596
597 $this->setExpectedException( ApiUsageException::class,
598 'Unrecognized value for parameter "contentformat": nonexistent format.' );
599
600 try {
601 $this->doApiRequestWithToken( [
602 'action' => 'edit',
603 'title' => $name,
604 'text' => 'some text',
605 'contentformat' => 'nonexistent format',
606 ] );
607 } finally {
608 $this->assertFalse( Title::newFromText( $name )->exists() );
609 }
610 }
611
612 public function testMismatchedContentFormat() {
613 $name = 'Help:' . ucfirst( __FUNCTION__ );
614
615 $this->setExpectedException( ApiUsageException::class,
616 'The requested format text/plain is not supported for content ' .
617 "model wikitext used by $name." );
618
619 try {
620 $this->doApiRequestWithToken( [
621 'action' => 'edit',
622 'title' => $name,
623 'text' => 'some text',
624 'contentmodel' => 'wikitext',
625 'contentformat' => 'text/plain',
626 ] );
627 } finally {
628 $this->assertFalse( Title::newFromText( $name )->exists() );
629 }
630 }
631
632 public function testUndoToInvalidRev() {
633 $name = 'Help:' . ucfirst( __FUNCTION__ );
634
635 $revId = $this->editPage( $name, 'Some text' )->value['revision']
636 ->getId();
637 $revId++;
638
639 $this->setExpectedException( ApiUsageException::class,
640 "There is no revision with ID $revId." );
641
642 $this->doApiRequestWithToken( [
643 'action' => 'edit',
644 'title' => $name,
645 'undo' => $revId,
646 ] );
647 }
648
649 /**
650 * Tests what happens if the undo parameter is a valid revision, but
651 * the undoafter parameter doesn't refer to a revision that exists in the
652 * database.
653 */
654 public function testUndoAfterToInvalidRev() {
655 // We can't just pick a large number for undoafter (as in
656 // testUndoToInvalidRev above), because then MediaWiki will helpfully
657 // assume we switched around undo and undoafter and we'll test the code
658 // path for undo being invalid, not undoafter. So instead we delete
659 // the revision from the database. In real life this case could come
660 // up if a revision number was skipped, e.g., if two transactions try
661 // to insert new revision rows at once and the first one to succeed
662 // gets rolled back.
663 $name = 'Help:' . ucfirst( __FUNCTION__ );
664 $titleObj = Title::newFromText( $name );
665
666 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
667 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
668 $revId3 = $this->editPage( $name, '3' )->value['revision']->getId();
669
670 // Make the middle revision disappear
671 $dbw = wfGetDB( DB_MASTER );
672 $dbw->delete( 'revision', [ 'rev_id' => $revId2 ], __METHOD__ );
673 $dbw->update( 'revision', [ 'rev_parent_id' => $revId1 ],
674 [ 'rev_id' => $revId3 ], __METHOD__ );
675
676 $this->setExpectedException( ApiUsageException::class,
677 "There is no revision with ID $revId2." );
678
679 $this->doApiRequestWithToken( [
680 'action' => 'edit',
681 'title' => $name,
682 'undo' => $revId3,
683 'undoafter' => $revId2,
684 ] );
685 }
686
687 /**
688 * Tests what happens if the undo parameter is a valid revision, but
689 * undoafter is hidden (rev_deleted).
690 */
691 public function testUndoAfterToHiddenRev() {
692 $name = 'Help:' . ucfirst( __FUNCTION__ );
693 $titleObj = Title::newFromText( $name );
694
695 $this->editPage( $name, '0' );
696
697 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
698
699 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
700
701 // Hide the middle revision
702 $list = RevisionDeleter::createList( 'revision',
703 RequestContext::getMain(), $titleObj, [ $revId1 ] );
704 $list->setVisibility( [
705 'value' => [ Revision::DELETED_TEXT => 1 ],
706 'comment' => 'Bye-bye',
707 ] );
708
709 $this->setExpectedException( ApiUsageException::class,
710 "There is no revision with ID $revId1." );
711
712 $this->doApiRequestWithToken( [
713 'action' => 'edit',
714 'title' => $name,
715 'undo' => $revId2,
716 'undoafter' => $revId1,
717 ] );
718 }
719
720 /**
721 * Test undo when a revision with a higher id has an earlier timestamp.
722 * This can happen if importing an old revision.
723 */
724 public function testUndoWithSwappedRevisions() {
725 $name = 'Help:' . ucfirst( __FUNCTION__ );
726 $titleObj = Title::newFromText( $name );
727
728 $this->editPage( $name, '0' );
729
730 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
731
732 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
733
734 // Now monkey with the timestamp
735 $dbw = wfGetDB( DB_MASTER );
736 $dbw->update(
737 'revision',
738 [ 'rev_timestamp' => wfTimestamp( TS_MW, time() - 86400 ) ],
739 [ 'rev_id' => $revId1 ],
740 __METHOD__
741 );
742
743 $this->doApiRequestWithToken( [
744 'action' => 'edit',
745 'title' => $name,
746 'undo' => $revId2,
747 'undoafter' => $revId1,
748 ] );
749
750 $text = ( new WikiPage( $titleObj ) )->getContent()->getNativeData();
751
752 // This is wrong! It should be 1. But let's test for our incorrect
753 // behavior for now, so if someone fixes it they'll fix the test as
754 // well to expect 1. If we disabled the test, it might stay disabled
755 // even once the bug is fixed, which would be a shame.
756 $this->assertSame( '2', $text );
757 }
758
759 public function testUndoWithConflicts() {
760 $name = 'Help:' . ucfirst( __FUNCTION__ );
761
762 $this->setExpectedException( ApiUsageException::class,
763 'The edit could not be undone due to conflicting intermediate edits.' );
764
765 $this->editPage( $name, '1' );
766
767 $revId = $this->editPage( $name, '2' )->value['revision']->getId();
768
769 $this->editPage( $name, '3' );
770
771 $this->doApiRequestWithToken( [
772 'action' => 'edit',
773 'title' => $name,
774 'undo' => $revId,
775 ] );
776
777 $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
778 ->getNativeData();
779 $this->assertSame( '3', $text );
780 }
781
782 /**
783 * undoafter is supposed to be less than undo. If not, we reverse their
784 * meaning, so that the two are effectively interchangeable.
785 */
786 public function testReversedUndoAfter() {
787 $name = 'Help:' . ucfirst( __FUNCTION__ );
788
789 $this->editPage( $name, '0' );
790 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
791 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
792
793 $this->doApiRequestWithToken( [
794 'action' => 'edit',
795 'title' => $name,
796 'undo' => $revId1,
797 'undoafter' => $revId2,
798 ] );
799
800 $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
801 ->getNativeData();
802 $this->assertSame( '1', $text );
803 }
804
805 public function testUndoToRevFromDifferentPage() {
806 $name = 'Help:' . ucfirst( __FUNCTION__ );
807
808 $this->editPage( "$name-1", 'Some text' );
809 $revId = $this->editPage( "$name-1", 'Some more text' )
810 ->value['revision']->getId();
811
812 $this->editPage( "$name-2", 'Some text' );
813
814 $this->setExpectedException( ApiUsageException::class,
815 "r$revId is not a revision of $name-2." );
816
817 $this->doApiRequestWithToken( [
818 'action' => 'edit',
819 'title' => "$name-2",
820 'undo' => $revId,
821 ] );
822 }
823
824 public function testUndoAfterToRevFromDifferentPage() {
825 $name = 'Help:' . ucfirst( __FUNCTION__ );
826
827 $revId1 = $this->editPage( "$name-1", 'Some text' )
828 ->value['revision']->getId();
829
830 $revId2 = $this->editPage( "$name-2", 'Some text' )
831 ->value['revision']->getId();
832
833 $this->setExpectedException( ApiUsageException::class,
834 "r$revId1 is not a revision of $name-2." );
835
836 $this->doApiRequestWithToken( [
837 'action' => 'edit',
838 'title' => "$name-2",
839 'undo' => $revId2,
840 'undoafter' => $revId1,
841 ] );
842 }
843
844 public function testMd5Text() {
845 $name = 'Help:' . ucfirst( __FUNCTION__ );
846
847 $this->assertFalse( Title::newFromText( $name )->exists() );
848
849 $this->doApiRequestWithToken( [
850 'action' => 'edit',
851 'title' => $name,
852 'text' => 'Some text',
853 'md5' => md5( 'Some text' ),
854 ] );
855
856 $this->assertTrue( Title::newFromText( $name )->exists() );
857 }
858
859 public function testMd5PrependText() {
860 $name = 'Help:' . ucfirst( __FUNCTION__ );
861
862 $this->editPage( $name, 'Some text' );
863
864 $this->doApiRequestWithToken( [
865 'action' => 'edit',
866 'title' => $name,
867 'prependtext' => 'Alert: ',
868 'md5' => md5( 'Alert: ' ),
869 ] );
870
871 $text = ( new WikiPage( Title::newFromText( $name ) ) )
872 ->getContent()->getNativeData();
873 $this->assertSame( 'Alert: Some text', $text );
874 }
875
876 public function testMd5AppendText() {
877 $name = 'Help:' . ucfirst( __FUNCTION__ );
878
879 $this->editPage( $name, 'Some text' );
880
881 $this->doApiRequestWithToken( [
882 'action' => 'edit',
883 'title' => $name,
884 'appendtext' => ' is nice',
885 'md5' => md5( ' is nice' ),
886 ] );
887
888 $text = ( new WikiPage( Title::newFromText( $name ) ) )
889 ->getContent()->getNativeData();
890 $this->assertSame( 'Some text is nice', $text );
891 }
892
893 public function testMd5PrependAndAppendText() {
894 $name = 'Help:' . ucfirst( __FUNCTION__ );
895
896 $this->editPage( $name, 'Some text' );
897
898 $this->doApiRequestWithToken( [
899 'action' => 'edit',
900 'title' => $name,
901 'prependtext' => 'Alert: ',
902 'appendtext' => ' is nice',
903 'md5' => md5( 'Alert: is nice' ),
904 ] );
905
906 $text = ( new WikiPage( Title::newFromText( $name ) ) )
907 ->getContent()->getNativeData();
908 $this->assertSame( 'Alert: Some text is nice', $text );
909 }
910
911 public function testIncorrectMd5Text() {
912 $name = 'Help:' . ucfirst( __FUNCTION__ );
913
914 $this->setExpectedException( ApiUsageException::class,
915 'The supplied MD5 hash was incorrect.' );
916
917 $this->doApiRequestWithToken( [
918 'action' => 'edit',
919 'title' => $name,
920 'text' => 'Some text',
921 'md5' => md5( '' ),
922 ] );
923 }
924
925 public function testIncorrectMd5PrependText() {
926 $name = 'Help:' . ucfirst( __FUNCTION__ );
927
928 $this->setExpectedException( ApiUsageException::class,
929 'The supplied MD5 hash was incorrect.' );
930
931 $this->doApiRequestWithToken( [
932 'action' => 'edit',
933 'title' => $name,
934 'prependtext' => 'Some ',
935 'appendtext' => 'text',
936 'md5' => md5( 'Some ' ),
937 ] );
938 }
939
940 public function testIncorrectMd5AppendText() {
941 $name = 'Help:' . ucfirst( __FUNCTION__ );
942
943 $this->setExpectedException( ApiUsageException::class,
944 'The supplied MD5 hash was incorrect.' );
945
946 $this->doApiRequestWithToken( [
947 'action' => 'edit',
948 'title' => $name,
949 'prependtext' => 'Some ',
950 'appendtext' => 'text',
951 'md5' => md5( 'text' ),
952 ] );
953 }
954
955 public function testCreateOnly() {
956 $name = 'Help:' . ucfirst( __FUNCTION__ );
957
958 $this->setExpectedException( ApiUsageException::class,
959 'The article you tried to create has been created already.' );
960
961 $this->editPage( $name, 'Some text' );
962 $this->assertTrue( Title::newFromText( $name )->exists() );
963
964 try {
965 $this->doApiRequestWithToken( [
966 'action' => 'edit',
967 'title' => $name,
968 'text' => 'Some more text',
969 'createonly' => '',
970 ] );
971 } finally {
972 // Validate that content was not changed
973 $text = ( new WikiPage( Title::newFromText( $name ) ) )
974 ->getContent()->getNativeData();
975
976 $this->assertSame( 'Some text', $text );
977 }
978 }
979
980 public function testNoCreate() {
981 $name = 'Help:' . ucfirst( __FUNCTION__ );
982
983 $this->setExpectedException( ApiUsageException::class,
984 "The page you specified doesn't exist." );
985
986 $this->assertFalse( Title::newFromText( $name )->exists() );
987
988 try {
989 $this->doApiRequestWithToken( [
990 'action' => 'edit',
991 'title' => $name,
992 'text' => 'Some text',
993 'nocreate' => '',
994 ] );
995 } finally {
996 $this->assertFalse( Title::newFromText( $name )->exists() );
997 }
998 }
999
1000 /**
1001 * Appending/prepending is currently only supported for TextContent. We
1002 * test this right now, and when support is added this test should be
1003 * replaced by tests that the support is correct.
1004 */
1005 public function testAppendWithNonTextContentHandler() {
1006 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1007
1008 $this->setExpectedException( ApiUsageException::class,
1009 "Can't append to pages using content model testing-nontext." );
1010
1011 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1012 function ( Title $title, &$model ) use ( $name ) {
1013 if ( $title->getPrefixedText() === $name ) {
1014 $model = 'testing-nontext';
1015 }
1016 return true;
1017 }
1018 );
1019
1020 $this->doApiRequestWithToken( [
1021 'action' => 'edit',
1022 'title' => $name,
1023 'appendtext' => 'Some text',
1024 ] );
1025 }
1026
1027 public function testAppendInMediaWikiNamespace() {
1028 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1029
1030 $this->assertFalse( Title::newFromText( $name )->exists() );
1031
1032 $this->doApiRequestWithToken( [
1033 'action' => 'edit',
1034 'title' => $name,
1035 'appendtext' => 'Some text',
1036 ] );
1037
1038 $this->assertTrue( Title::newFromText( $name )->exists() );
1039 }
1040
1041 public function testAppendInMediaWikiNamespaceWithSerializationError() {
1042 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1043
1044 $this->setExpectedException( ApiUsageException::class,
1045 'Content serialization failed: Could not unserialize content' );
1046
1047 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1048 function ( Title $title, &$model ) use ( $name ) {
1049 if ( $title->getPrefixedText() === $name ) {
1050 $model = 'testing-serialize-error';
1051 }
1052 return true;
1053 }
1054 );
1055
1056 $this->doApiRequestWithToken( [
1057 'action' => 'edit',
1058 'title' => $name,
1059 'appendtext' => 'Some text',
1060 ] );
1061 }
1062
1063 public function testAppendNewSection() {
1064 $name = 'Help:' . ucfirst( __FUNCTION__ );
1065
1066 $this->editPage( $name, 'Initial content' );
1067
1068 $this->doApiRequestWithToken( [
1069 'action' => 'edit',
1070 'title' => $name,
1071 'appendtext' => '== New section ==',
1072 'section' => 'new',
1073 ] );
1074
1075 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1076 ->getContent()->getNativeData();
1077
1078 $this->assertSame( "Initial content\n\n== New section ==", $text );
1079 }
1080
1081 public function testAppendNewSectionWithInvalidContentModel() {
1082 $name = 'Help:' . ucfirst( __FUNCTION__ );
1083
1084 $this->setExpectedException( ApiUsageException::class,
1085 'Sections are not supported for content model text.' );
1086
1087 $this->editPage( $name, 'Initial content' );
1088
1089 $this->doApiRequestWithToken( [
1090 'action' => 'edit',
1091 'title' => $name,
1092 'appendtext' => '== New section ==',
1093 'section' => 'new',
1094 'contentmodel' => 'text',
1095 ] );
1096 }
1097
1098 public function testAppendNewSectionWithTitle() {
1099 $name = 'Help:' . ucfirst( __FUNCTION__ );
1100
1101 $this->editPage( $name, 'Initial content' );
1102
1103 $this->doApiRequestWithToken( [
1104 'action' => 'edit',
1105 'title' => $name,
1106 'sectiontitle' => 'My section',
1107 'appendtext' => 'More content',
1108 'section' => 'new',
1109 ] );
1110
1111 $page = new WikiPage( Title::newFromText( $name ) );
1112
1113 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1114 $page->getContent()->getNativeData() );
1115 $this->assertSame( '/* My section */ new section',
1116 $page->getRevision()->getComment() );
1117 }
1118
1119 public function testAppendNewSectionWithSummary() {
1120 $name = 'Help:' . ucfirst( __FUNCTION__ );
1121
1122 $this->editPage( $name, 'Initial content' );
1123
1124 $this->doApiRequestWithToken( [
1125 'action' => 'edit',
1126 'title' => $name,
1127 'appendtext' => 'More content',
1128 'section' => 'new',
1129 'summary' => 'Add new section',
1130 ] );
1131
1132 $page = new WikiPage( Title::newFromText( $name ) );
1133
1134 $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content",
1135 $page->getContent()->getNativeData() );
1136 // EditPage actually assumes the summary is the section name here
1137 $this->assertSame( '/* Add new section */ new section',
1138 $page->getRevision()->getComment() );
1139 }
1140
1141 public function testAppendNewSectionWithTitleAndSummary() {
1142 $name = 'Help:' . ucfirst( __FUNCTION__ );
1143
1144 $this->editPage( $name, 'Initial content' );
1145
1146 $this->doApiRequestWithToken( [
1147 'action' => 'edit',
1148 'title' => $name,
1149 'sectiontitle' => 'My section',
1150 'appendtext' => 'More content',
1151 'section' => 'new',
1152 'summary' => 'Add new section',
1153 ] );
1154
1155 $page = new WikiPage( Title::newFromText( $name ) );
1156
1157 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1158 $page->getContent()->getNativeData() );
1159 $this->assertSame( 'Add new section',
1160 $page->getRevision()->getComment() );
1161 }
1162
1163 public function testAppendToSection() {
1164 $name = 'Help:' . ucfirst( __FUNCTION__ );
1165
1166 $this->editPage( $name, "== Section 1 ==\n\nContent\n\n" .
1167 "== Section 2 ==\n\nFascinating!" );
1168
1169 $this->doApiRequestWithToken( [
1170 'action' => 'edit',
1171 'title' => $name,
1172 'appendtext' => ' and more content',
1173 'section' => '1',
1174 ] );
1175
1176 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1177 ->getContent()->getNativeData();
1178
1179 $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" .
1180 "== Section 2 ==\n\nFascinating!", $text );
1181 }
1182
1183 public function testAppendToFirstSection() {
1184 $name = 'Help:' . ucfirst( __FUNCTION__ );
1185
1186 $this->editPage( $name, "Content\n\n== Section 1 ==\n\nFascinating!" );
1187
1188 $this->doApiRequestWithToken( [
1189 'action' => 'edit',
1190 'title' => $name,
1191 'appendtext' => ' and more content',
1192 'section' => '0',
1193 ] );
1194
1195 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1196 ->getContent()->getNativeData();
1197
1198 $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" .
1199 "Fascinating!", $text );
1200 }
1201
1202 public function testAppendToNonexistentSection() {
1203 $name = 'Help:' . ucfirst( __FUNCTION__ );
1204
1205 $this->setExpectedException( ApiUsageException::class, 'There is no section 1.' );
1206
1207 $this->editPage( $name, 'Content' );
1208
1209 try {
1210 $this->doApiRequestWithToken( [
1211 'action' => 'edit',
1212 'title' => $name,
1213 'appendtext' => ' and more content',
1214 'section' => '1',
1215 ] );
1216 } finally {
1217 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1218 ->getContent()->getNativeData();
1219
1220 $this->assertSame( 'Content', $text );
1221 }
1222 }
1223
1224 public function testEditMalformedSection() {
1225 $name = 'Help:' . ucfirst( __FUNCTION__ );
1226
1227 $this->setExpectedException( ApiUsageException::class,
1228 'The "section" parameter must be a valid section ID or "new".' );
1229 $this->editPage( $name, 'Content' );
1230
1231 try {
1232 $this->doApiRequestWithToken( [
1233 'action' => 'edit',
1234 'title' => $name,
1235 'text' => 'Different content',
1236 'section' => 'It is unlikely that this is valid',
1237 ] );
1238 } finally {
1239 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1240 ->getContent()->getNativeData();
1241
1242 $this->assertSame( 'Content', $text );
1243 }
1244 }
1245
1246 public function testEditWithStartTimestamp() {
1247 $name = 'Help:' . ucfirst( __FUNCTION__ );
1248 $this->setExpectedException( ApiUsageException::class,
1249 'The page has been deleted since you fetched its timestamp.' );
1250
1251 $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
1252
1253 $this->editPage( $name, 'Some text' );
1254
1255 $pageObj = new WikiPage( Title::newFromText( $name ) );
1256 $pageObj->doDeleteArticle( 'Bye-bye' );
1257
1258 $this->assertFalse( $pageObj->exists() );
1259
1260 try {
1261 $this->doApiRequestWithToken( [
1262 'action' => 'edit',
1263 'title' => $name,
1264 'text' => 'Different text',
1265 'starttimestamp' => $startTime,
1266 ] );
1267 } finally {
1268 $this->assertFalse( $pageObj->exists() );
1269 }
1270 }
1271
1272 public function testEditMinor() {
1273 $name = 'Help:' . ucfirst( __FUNCTION__ );
1274
1275 $this->editPage( $name, 'Some text' );
1276
1277 $this->doApiRequestWithToken( [
1278 'action' => 'edit',
1279 'title' => $name,
1280 'text' => 'Different text',
1281 'minor' => '',
1282 ] );
1283
1284 $revisionStore = \MediaWiki\MediaWikiServices::getInstance()->getRevisionStore();
1285 $revision = $revisionStore->getRevisionByTitle( Title::newFromText( $name ) );
1286 $this->assertTrue( $revision->isMinor() );
1287 }
1288
1289 public function testEditRecreate() {
1290 $name = 'Help:' . ucfirst( __FUNCTION__ );
1291
1292 $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
1293
1294 $this->editPage( $name, 'Some text' );
1295
1296 $pageObj = new WikiPage( Title::newFromText( $name ) );
1297 $pageObj->doDeleteArticle( 'Bye-bye' );
1298
1299 $this->assertFalse( $pageObj->exists() );
1300
1301 $this->doApiRequestWithToken( [
1302 'action' => 'edit',
1303 'title' => $name,
1304 'text' => 'Different text',
1305 'starttimestamp' => $startTime,
1306 'recreate' => '',
1307 ] );
1308
1309 $this->assertTrue( Title::newFromText( $name )->exists() );
1310 }
1311
1312 public function testEditWatch() {
1313 $name = 'Help:' . ucfirst( __FUNCTION__ );
1314 $user = self::$users['sysop']->getUser();
1315
1316 $this->doApiRequestWithToken( [
1317 'action' => 'edit',
1318 'title' => $name,
1319 'text' => 'Some text',
1320 'watch' => '',
1321 ] );
1322
1323 $this->assertTrue( Title::newFromText( $name )->exists() );
1324 $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
1325 }
1326
1327 public function testEditUnwatch() {
1328 $name = 'Help:' . ucfirst( __FUNCTION__ );
1329 $user = self::$users['sysop']->getUser();
1330 $titleObj = Title::newFromText( $name );
1331
1332 $user->addWatch( $titleObj );
1333
1334 $this->assertFalse( $titleObj->exists() );
1335 $this->assertTrue( $user->isWatched( $titleObj ) );
1336
1337 $this->doApiRequestWithToken( [
1338 'action' => 'edit',
1339 'title' => $name,
1340 'text' => 'Some text',
1341 'unwatch' => '',
1342 ] );
1343
1344 $this->assertTrue( $titleObj->exists() );
1345 $this->assertFalse( $user->isWatched( $titleObj ) );
1346 }
1347
1348 public function testEditWithTag() {
1349 $name = 'Help:' . ucfirst( __FUNCTION__ );
1350
1351 ChangeTags::defineTag( 'custom tag' );
1352
1353 $revId = $this->doApiRequestWithToken( [
1354 'action' => 'edit',
1355 'title' => $name,
1356 'text' => 'Some text',
1357 'tags' => 'custom tag',
1358 ] )[0]['edit']['newrevid'];
1359
1360 $dbw = wfGetDB( DB_MASTER );
1361 $this->assertSame( 'custom tag', $dbw->selectField(
1362 'change_tag', 'ct_tag', [ 'ct_rev_id' => $revId ], __METHOD__ ) );
1363 }
1364
1365 public function testEditWithoutTagPermission() {
1366 $name = 'Help:' . ucfirst( __FUNCTION__ );
1367
1368 $this->setExpectedException( ApiUsageException::class,
1369 'You do not have permission to apply change tags along with your changes.' );
1370
1371 $this->assertFalse( Title::newFromText( $name )->exists() );
1372
1373 ChangeTags::defineTag( 'custom tag' );
1374 $this->setMwGlobals( 'wgRevokePermissions',
1375 [ 'user' => [ 'applychangetags' => true ] ] );
1376 try {
1377 $this->doApiRequestWithToken( [
1378 'action' => 'edit',
1379 'title' => $name,
1380 'text' => 'Some text',
1381 'tags' => 'custom tag',
1382 ] );
1383 } finally {
1384 $this->assertFalse( Title::newFromText( $name )->exists() );
1385 }
1386 }
1387
1388 public function testEditAbortedByHook() {
1389 $name = 'Help:' . ucfirst( __FUNCTION__ );
1390
1391 $this->setExpectedException( ApiUsageException::class,
1392 'The modification you tried to make was aborted by an extension.' );
1393
1394 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1395 'hook-APIEditBeforeSave-closure)' );
1396
1397 $this->setTemporaryHook( 'APIEditBeforeSave',
1398 function () {
1399 return false;
1400 }
1401 );
1402
1403 try {
1404 $this->doApiRequestWithToken( [
1405 'action' => 'edit',
1406 'title' => $name,
1407 'text' => 'Some text',
1408 ] );
1409 } finally {
1410 $this->assertFalse( Title::newFromText( $name )->exists() );
1411 }
1412 }
1413
1414 public function testEditAbortedByHookWithCustomOutput() {
1415 $name = 'Help:' . ucfirst( __FUNCTION__ );
1416
1417 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1418 'hook-APIEditBeforeSave-closure)' );
1419
1420 $this->setTemporaryHook( 'APIEditBeforeSave',
1421 function ( $unused1, $unused2, &$r ) {
1422 $r['msg'] = 'Some message';
1423 return false;
1424 } );
1425
1426 $result = $this->doApiRequestWithToken( [
1427 'action' => 'edit',
1428 'title' => $name,
1429 'text' => 'Some text',
1430 ] );
1431 Wikimedia\restoreWarnings();
1432
1433 $this->assertSame( [ 'msg' => 'Some message', 'result' => 'Failure' ],
1434 $result[0]['edit'] );
1435
1436 $this->assertFalse( Title::newFromText( $name )->exists() );
1437 }
1438
1439 public function testEditAbortedByEditPageHookWithResult() {
1440 $name = 'Help:' . ucfirst( __FUNCTION__ );
1441
1442 $this->setTemporaryHook( 'EditFilterMergedContent',
1443 function ( $unused1, $unused2, Status $status ) {
1444 $status->apiHookResult = [ 'msg' => 'A message for you!' ];
1445 return false;
1446 } );
1447
1448 $res = $this->doApiRequestWithToken( [
1449 'action' => 'edit',
1450 'title' => $name,
1451 'text' => 'Some text',
1452 ] );
1453
1454 $this->assertFalse( Title::newFromText( $name )->exists() );
1455 $this->assertSame( [ 'edit' => [ 'msg' => 'A message for you!',
1456 'result' => 'Failure' ] ], $res[0] );
1457 }
1458
1459 public function testEditAbortedByEditPageHookWithNoResult() {
1460 $name = 'Help:' . ucfirst( __FUNCTION__ );
1461
1462 $this->setExpectedException( ApiUsageException::class,
1463 'The modification you tried to make was aborted by an extension.' );
1464
1465 $this->setTemporaryHook( 'EditFilterMergedContent',
1466 function () {
1467 return false;
1468 }
1469 );
1470
1471 try {
1472 $this->doApiRequestWithToken( [
1473 'action' => 'edit',
1474 'title' => $name,
1475 'text' => 'Some text',
1476 ] );
1477 } finally {
1478 $this->assertFalse( Title::newFromText( $name )->exists() );
1479 }
1480 }
1481
1482 public function testEditWhileBlocked() {
1483 $name = 'Help:' . ucfirst( __FUNCTION__ );
1484
1485 $this->setExpectedException( ApiUsageException::class,
1486 'You have been blocked from editing.' );
1487
1488 $block = new Block( [
1489 'address' => self::$users['sysop']->getUser()->getName(),
1490 'by' => self::$users['sysop']->getUser()->getId(),
1491 'reason' => 'Capriciousness',
1492 'timestamp' => '19370101000000',
1493 'expiry' => 'infinity',
1494 ] );
1495 $block->insert();
1496
1497 try {
1498 $this->doApiRequestWithToken( [
1499 'action' => 'edit',
1500 'title' => $name,
1501 'text' => 'Some text',
1502 ] );
1503 } finally {
1504 $block->delete();
1505 self::$users['sysop']->getUser()->clearInstanceCache();
1506 }
1507 }
1508
1509 public function testEditWhileReadOnly() {
1510 $name = 'Help:' . ucfirst( __FUNCTION__ );
1511
1512 $this->setExpectedException( ApiUsageException::class,
1513 'The wiki is currently in read-only mode.' );
1514
1515 $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1516 $svc->setReason( "Read-only for testing" );
1517
1518 try {
1519 $this->doApiRequestWithToken( [
1520 'action' => 'edit',
1521 'title' => $name,
1522 'text' => 'Some text',
1523 ] );
1524 } finally {
1525 $svc->setReason( false );
1526 }
1527 }
1528
1529 public function testCreateImageRedirectAnon() {
1530 $name = 'File:' . ucfirst( __FUNCTION__ );
1531
1532 $this->setExpectedException( ApiUsageException::class,
1533 "Anonymous users can't create image redirects." );
1534
1535 $this->doApiRequestWithToken( [
1536 'action' => 'edit',
1537 'title' => $name,
1538 'text' => '#REDIRECT [[File:Other file.png]]',
1539 ], null, new User() );
1540 }
1541
1542 public function testCreateImageRedirectLoggedIn() {
1543 $name = 'File:' . ucfirst( __FUNCTION__ );
1544
1545 $this->setExpectedException( ApiUsageException::class,
1546 "You don't have permission to create image redirects." );
1547
1548 $this->setMwGlobals( 'wgRevokePermissions',
1549 [ 'user' => [ 'upload' => true ] ] );
1550
1551 $this->doApiRequestWithToken( [
1552 'action' => 'edit',
1553 'title' => $name,
1554 'text' => '#REDIRECT [[File:Other file.png]]',
1555 ] );
1556 }
1557
1558 public function testTooBigEdit() {
1559 $name = 'Help:' . ucfirst( __FUNCTION__ );
1560
1561 $this->setExpectedException( ApiUsageException::class,
1562 'The content you supplied exceeds the article size limit of 1 kilobyte.' );
1563
1564 $this->setMwGlobals( 'wgMaxArticleSize', 1 );
1565
1566 $text = str_repeat( '!', 1025 );
1567
1568 $this->doApiRequestWithToken( [
1569 'action' => 'edit',
1570 'title' => $name,
1571 'text' => $text,
1572 ] );
1573 }
1574
1575 public function testProhibitedAnonymousEdit() {
1576 $name = 'Help:' . ucfirst( __FUNCTION__ );
1577
1578 $this->setExpectedException( ApiUsageException::class,
1579 'The action you have requested is limited to users in the group: ' );
1580
1581 $this->setMwGlobals( 'wgRevokePermissions', [ '*' => [ 'edit' => true ] ] );
1582
1583 $this->doApiRequestWithToken( [
1584 'action' => 'edit',
1585 'title' => $name,
1586 'text' => 'Some text',
1587 ], null, new User() );
1588 }
1589
1590 public function testProhibitedChangeContentModel() {
1591 $name = 'Help:' . ucfirst( __FUNCTION__ );
1592
1593 $this->setExpectedException( ApiUsageException::class,
1594 "You don't have permission to change the content model of a page." );
1595
1596 $this->setMwGlobals( 'wgRevokePermissions',
1597 [ 'user' => [ 'editcontentmodel' => true ] ] );
1598
1599 $this->doApiRequestWithToken( [
1600 'action' => 'edit',
1601 'title' => $name,
1602 'text' => 'Some text',
1603 'contentmodel' => 'json',
1604 ] );
1605 }
1606 }