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