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