MediaWiki REL1_31
ApiEditPageTest.php
Go to the documentation of this file.
1<?php
2
15
16 protected function setUp() {
18
19 parent::setUp();
20
21 $this->setMwGlobals( [
22 'wgExtraNamespaces' => $wgExtraNamespaces,
23 'wgNamespaceContentModels' => $wgNamespaceContentModels,
24 'wgContentHandlers' => $wgContentHandlers,
25 'wgContLang' => $wgContLang,
26 ] );
27
28 $wgExtraNamespaces[12312] = 'Dummy';
29 $wgExtraNamespaces[12313] = 'Dummy_talk';
30 $wgExtraNamespaces[12314] = 'DummyNonText';
31 $wgExtraNamespaces[12315] = 'DummyNonText_talk';
32
33 $wgNamespaceContentModels[12312] = "testing";
34 $wgNamespaceContentModels[12314] = "testing-nontext";
35
36 $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
37 $wgContentHandlers["testing-nontext"] = 'DummyNonTextContentHandler';
38 $wgContentHandlers["testing-serialize-error"] =
39 'DummySerializeErrorContentHandler';
40
41 MWNamespace::clearCaches();
42 $wgContLang->resetNamespaces(); # reset namespace cache
43 }
44
45 protected function tearDown() {
46 global $wgContLang;
47
48 MWNamespace::clearCaches();
49 $wgContLang->resetNamespaces(); # reset namespace cache
50
51 parent::tearDown();
52 }
53
54 public function testEdit() {
55 $name = 'Help:ApiEditPageTest_testEdit'; // assume Help namespace to default to wikitext
56
57 // -- test new page --------------------------------------------
58 $apiResult = $this->doApiRequestWithToken( [
59 'action' => 'edit',
60 'title' => $name,
61 'text' => 'some text',
62 ] );
63 $apiResult = $apiResult[0];
64
65 // Validate API result data
66 $this->assertArrayHasKey( 'edit', $apiResult );
67 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
68 $this->assertSame( 'Success', $apiResult['edit']['result'] );
69
70 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
71 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
72
73 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
74
75 // -- test existing page, no change ----------------------------
76 $data = $this->doApiRequestWithToken( [
77 'action' => 'edit',
78 'title' => $name,
79 'text' => 'some text',
80 ] );
81
82 $this->assertSame( 'Success', $data[0]['edit']['result'] );
83
84 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
85 $this->assertArrayHasKey( 'nochange', $data[0]['edit'] );
86
87 // -- test existing page, with change --------------------------
88 $data = $this->doApiRequestWithToken( [
89 'action' => 'edit',
90 'title' => $name,
91 'text' => 'different text'
92 ] );
93
94 $this->assertSame( 'Success', $data[0]['edit']['result'] );
95
96 $this->assertArrayNotHasKey( 'new', $data[0]['edit'] );
97 $this->assertArrayNotHasKey( 'nochange', $data[0]['edit'] );
98
99 $this->assertArrayHasKey( 'oldrevid', $data[0]['edit'] );
100 $this->assertArrayHasKey( 'newrevid', $data[0]['edit'] );
101 $this->assertNotEquals(
102 $data[0]['edit']['newrevid'],
103 $data[0]['edit']['oldrevid'],
104 "revision id should change after edit"
105 );
106 }
107
111 public static function provideEditAppend() {
112 return [
113 [ # 0: append
114 'foo', 'append', 'bar', "foobar"
115 ],
116 [ # 1: prepend
117 'foo', 'prepend', 'bar', "barfoo"
118 ],
119 [ # 2: append to empty page
120 '', 'append', 'foo', "foo"
121 ],
122 [ # 3: prepend to empty page
123 '', 'prepend', 'foo', "foo"
124 ],
125 [ # 4: append to non-existing page
126 null, 'append', 'foo', "foo"
127 ],
128 [ # 5: prepend to non-existing page
129 null, 'prepend', 'foo', "foo"
130 ],
131 ];
132 }
133
137 public function testEditAppend( $text, $op, $append, $expected ) {
138 static $count = 0;
139 $count++;
140
141 // assume NS_HELP defaults to wikitext
142 $name = "Help:ApiEditPageTest_testEditAppend_$count";
143
144 // -- create page (or not) -----------------------------------------
145 if ( $text !== null ) {
146 list( $re ) = $this->doApiRequestWithToken( [
147 'action' => 'edit',
148 'title' => $name,
149 'text' => $text, ] );
150
151 $this->assertSame( 'Success', $re['edit']['result'] ); // sanity
152 }
153
154 // -- try append/prepend --------------------------------------------
155 list( $re ) = $this->doApiRequestWithToken( [
156 'action' => 'edit',
157 'title' => $name,
158 $op . 'text' => $append, ] );
159
160 $this->assertSame( 'Success', $re['edit']['result'] );
161
162 // -- validate -----------------------------------------------------
163 $page = new WikiPage( Title::newFromText( $name ) );
164 $content = $page->getContent();
165 $this->assertNotNull( $content, 'Page should have been created' );
166
167 $text = $content->getNativeData();
168
169 $this->assertSame( $expected, $text );
170 }
171
175 public function testEditSection() {
176 $name = 'Help:ApiEditPageTest_testEditSection';
177 $page = WikiPage::factory( Title::newFromText( $name ) );
178 $text = "==section 1==\ncontent 1\n==section 2==\ncontent2";
179 // Preload the page with some text
180 $page->doEditContent( ContentHandler::makeContent( $text, $page->getTitle() ), 'summary' );
181
182 list( $re ) = $this->doApiRequestWithToken( [
183 'action' => 'edit',
184 'title' => $name,
185 'section' => '1',
186 'text' => "==section 1==\nnew content 1",
187 ] );
188 $this->assertSame( 'Success', $re['edit']['result'] );
189 $newtext = WikiPage::factory( Title::newFromText( $name ) )
190 ->getContent( Revision::RAW )
191 ->getNativeData();
192 $this->assertSame( "==section 1==\nnew content 1\n\n==section 2==\ncontent2", $newtext );
193
194 // Test that we raise a 'nosuchsection' error
195 try {
196 $this->doApiRequestWithToken( [
197 'action' => 'edit',
198 'title' => $name,
199 'section' => '9999',
200 'text' => 'text',
201 ] );
202 $this->fail( "Should have raised an ApiUsageException" );
203 } catch ( ApiUsageException $e ) {
204 $this->assertTrue( self::apiExceptionHasCode( $e, 'nosuchsection' ) );
205 }
206 }
207
214 public function testEditNewSection() {
215 $name = 'Help:ApiEditPageTest_testEditNewSection';
216
217 // Test on a page that does not already exist
218 $this->assertFalse( Title::newFromText( $name )->exists() );
219 list( $re ) = $this->doApiRequestWithToken( [
220 'action' => 'edit',
221 'title' => $name,
222 'section' => 'new',
223 'text' => 'test',
224 'summary' => 'header',
225 ] );
226
227 $this->assertSame( 'Success', $re['edit']['result'] );
228 // Check the page text is correct
229 $text = WikiPage::factory( Title::newFromText( $name ) )
230 ->getContent( Revision::RAW )
231 ->getNativeData();
232 $this->assertSame( "== header ==\n\ntest", $text );
233
234 // Now on one that does
235 $this->assertTrue( Title::newFromText( $name )->exists() );
236 list( $re2 ) = $this->doApiRequestWithToken( [
237 'action' => 'edit',
238 'title' => $name,
239 'section' => 'new',
240 'text' => 'test',
241 'summary' => 'header',
242 ] );
243
244 $this->assertSame( 'Success', $re2['edit']['result'] );
245 $text = WikiPage::factory( Title::newFromText( $name ) )
246 ->getContent( Revision::RAW )
247 ->getNativeData();
248 $this->assertSame( "== header ==\n\ntest\n\n== header ==\n\ntest", $text );
249 }
250
254 public function testEdit_redirect() {
255 static $count = 0;
256 $count++;
257
258 // assume NS_HELP defaults to wikitext
259 $name = "Help:ApiEditPageTest_testEdit_redirect_$count";
260 $title = Title::newFromText( $name );
261 $page = WikiPage::factory( $title );
262
263 $rname = "Help:ApiEditPageTest_testEdit_redirect_r$count";
264 $rtitle = Title::newFromText( $rname );
265 $rpage = WikiPage::factory( $rtitle );
266
267 // base edit for content
268 $page->doEditContent( new WikitextContent( "Foo" ),
269 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
270 $this->forceRevisionDate( $page, '20120101000000' );
271 $baseTime = $page->getRevision()->getTimestamp();
272
273 // base edit for redirect
274 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
275 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
276 $this->forceRevisionDate( $rpage, '20120101000000' );
277
278 // conflicting edit to redirect
279 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
280 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
281 $this->forceRevisionDate( $rpage, '20120101020202' );
282
283 // try to save edit, following the redirect
284 list( $re, , ) = $this->doApiRequestWithToken( [
285 'action' => 'edit',
286 'title' => $rname,
287 'text' => 'nix bar!',
288 'basetimestamp' => $baseTime,
289 'section' => 'new',
290 'redirect' => true,
291 ] );
292
293 $this->assertSame( 'Success', $re['edit']['result'],
294 "no problems expected when following redirect" );
295 }
296
300 public function testEdit_redirectText() {
301 static $count = 0;
302 $count++;
303
304 // assume NS_HELP defaults to wikitext
305 $name = "Help:ApiEditPageTest_testEdit_redirectText_$count";
306 $title = Title::newFromText( $name );
307 $page = WikiPage::factory( $title );
308
309 $rname = "Help:ApiEditPageTest_testEdit_redirectText_r$count";
310 $rtitle = Title::newFromText( $rname );
311 $rpage = WikiPage::factory( $rtitle );
312
313 // base edit for content
314 $page->doEditContent( new WikitextContent( "Foo" ),
315 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
316 $this->forceRevisionDate( $page, '20120101000000' );
317 $baseTime = $page->getRevision()->getTimestamp();
318
319 // base edit for redirect
320 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
321 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
322 $this->forceRevisionDate( $rpage, '20120101000000' );
323
324 // conflicting edit to redirect
325 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]\n\n[[Category:Test]]" ),
326 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
327 $this->forceRevisionDate( $rpage, '20120101020202' );
328
329 // try to save edit, following the redirect but without creating a section
330 try {
331 $this->doApiRequestWithToken( [
332 'action' => 'edit',
333 'title' => $rname,
334 'text' => 'nix bar!',
335 'basetimestamp' => $baseTime,
336 'redirect' => true,
337 ] );
338
339 $this->fail( 'redirect-appendonly error expected' );
340 } catch ( ApiUsageException $ex ) {
341 $this->assertTrue( self::apiExceptionHasCode( $ex, 'redirect-appendonly' ) );
342 }
343 }
344
345 public function testEditConflict() {
346 static $count = 0;
347 $count++;
348
349 // assume NS_HELP defaults to wikitext
350 $name = "Help:ApiEditPageTest_testEditConflict_$count";
351 $title = Title::newFromText( $name );
352
353 $page = WikiPage::factory( $title );
354
355 // base edit
356 $page->doEditContent( new WikitextContent( "Foo" ),
357 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
358 $this->forceRevisionDate( $page, '20120101000000' );
359 $baseTime = $page->getRevision()->getTimestamp();
360
361 // conflicting edit
362 $page->doEditContent( new WikitextContent( "Foo bar" ),
363 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
364 $this->forceRevisionDate( $page, '20120101020202' );
365
366 // try to save edit, expect conflict
367 try {
368 $this->doApiRequestWithToken( [
369 'action' => 'edit',
370 'title' => $name,
371 'text' => 'nix bar!',
372 'basetimestamp' => $baseTime,
373 ] );
374
375 $this->fail( 'edit conflict expected' );
376 } catch ( ApiUsageException $ex ) {
377 $this->assertTrue( self::apiExceptionHasCode( $ex, 'editconflict' ) );
378 }
379 }
380
384 public function testEditConflict_newSection() {
385 static $count = 0;
386 $count++;
387
388 // assume NS_HELP defaults to wikitext
389 $name = "Help:ApiEditPageTest_testEditConflict_newSection_$count";
390 $title = Title::newFromText( $name );
391
392 $page = WikiPage::factory( $title );
393
394 // base edit
395 $page->doEditContent( new WikitextContent( "Foo" ),
396 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
397 $this->forceRevisionDate( $page, '20120101000000' );
398 $baseTime = $page->getRevision()->getTimestamp();
399
400 // conflicting edit
401 $page->doEditContent( new WikitextContent( "Foo bar" ),
402 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
403 $this->forceRevisionDate( $page, '20120101020202' );
404
405 // try to save edit, expect no conflict
406 list( $re, , ) = $this->doApiRequestWithToken( [
407 'action' => 'edit',
408 'title' => $name,
409 'text' => 'nix bar!',
410 'basetimestamp' => $baseTime,
411 'section' => 'new',
412 ] );
413
414 $this->assertSame( 'Success', $re['edit']['result'],
415 "no edit conflict expected here" );
416 }
417
418 public function testEditConflict_bug41990() {
419 static $count = 0;
420 $count++;
421
422 /*
423 * T43990: if the target page has a newer revision than the redirect, then editing the
424 * redirect while specifying 'redirect' and *not* specifying 'basetimestamp' erroneously
425 * caused an edit conflict to be detected.
426 */
427
428 // assume NS_HELP defaults to wikitext
429 $name = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_$count";
430 $title = Title::newFromText( $name );
431 $page = WikiPage::factory( $title );
432
433 $rname = "Help:ApiEditPageTest_testEditConflict_redirect_bug41990_r$count";
434 $rtitle = Title::newFromText( $rname );
435 $rpage = WikiPage::factory( $rtitle );
436
437 // base edit for content
438 $page->doEditContent( new WikitextContent( "Foo" ),
439 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
440 $this->forceRevisionDate( $page, '20120101000000' );
441
442 // base edit for redirect
443 $rpage->doEditContent( new WikitextContent( "#REDIRECT [[$name]]" ),
444 "testing 1", EDIT_NEW, false, self::$users['sysop']->getUser() );
445 $this->forceRevisionDate( $rpage, '20120101000000' );
446
447 // new edit to content
448 $page->doEditContent( new WikitextContent( "Foo bar" ),
449 "testing 2", EDIT_UPDATE, $page->getLatest(), self::$users['uploader']->getUser() );
450 $this->forceRevisionDate( $rpage, '20120101020202' );
451
452 // try to save edit; should work, following the redirect.
453 list( $re, , ) = $this->doApiRequestWithToken( [
454 'action' => 'edit',
455 'title' => $rname,
456 'text' => 'nix bar!',
457 'section' => 'new',
458 'redirect' => true,
459 ] );
460
461 $this->assertSame( 'Success', $re['edit']['result'],
462 "no edit conflict expected here" );
463 }
464
469 protected function forceRevisionDate( WikiPage $page, $timestamp ) {
470 $dbw = wfGetDB( DB_MASTER );
471
472 $dbw->update( 'revision',
473 [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
474 [ 'rev_id' => $page->getLatest() ] );
475
476 $page->clear();
477 }
478
480 $this->setExpectedException(
481 ApiUsageException::class,
482 'Direct editing via API is not supported for content model ' .
483 'testing used by Dummy:ApiEditPageTest_nonTextPageEdit'
484 );
485
486 $this->doApiRequestWithToken( [
487 'action' => 'edit',
488 'title' => 'Dummy:ApiEditPageTest_nonTextPageEdit',
489 'text' => '{"animals":["kittens!"]}'
490 ] );
491 }
492
494 $name = 'DummyNonText:ApiEditPageTest_testNonTextEdit';
495 $data = serialize( 'some bla bla text' );
496
497 $result = $this->doApiRequestWithToken( [
498 'action' => 'edit',
499 'title' => $name,
500 'text' => $data,
501 ] );
502
503 $apiResult = $result[0];
504
505 // Validate API result data
506 $this->assertArrayHasKey( 'edit', $apiResult );
507 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
508 $this->assertSame( 'Success', $apiResult['edit']['result'] );
509
510 $this->assertArrayHasKey( 'new', $apiResult['edit'] );
511 $this->assertArrayNotHasKey( 'nochange', $apiResult['edit'] );
512
513 $this->assertArrayHasKey( 'pageid', $apiResult['edit'] );
514
515 // validate resulting revision
516 $page = WikiPage::factory( Title::newFromText( $name ) );
517 $this->assertSame( "testing-nontext", $page->getContentModel() );
518 $this->assertSame( $data, $page->getContent()->serialize() );
519 }
520
527 $name = 'Help:' . __FUNCTION__;
528 $uploader = self::$users['uploader']->getUser();
529 $sysop = self::$users['sysop']->getUser();
530
531 $apiResult = $this->doApiRequestWithToken( [
532 'action' => 'edit',
533 'title' => $name,
534 'text' => 'some text',
535 ], null, $sysop )[0];
536
537 // Check success
538 $this->assertArrayHasKey( 'edit', $apiResult );
539 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
540 $this->assertSame( 'Success', $apiResult['edit']['result'] );
541 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
542 // Content model is wikitext
543 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
544
545 // Convert the page to JSON
546 $apiResult = $this->doApiRequestWithToken( [
547 'action' => 'edit',
548 'title' => $name,
549 'text' => '{}',
550 'contentmodel' => 'json',
551 ], null, $uploader )[0];
552
553 // Check success
554 $this->assertArrayHasKey( 'edit', $apiResult );
555 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
556 $this->assertSame( 'Success', $apiResult['edit']['result'] );
557 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
558 $this->assertSame( 'json', $apiResult['edit']['contentmodel'] );
559
560 $apiResult = $this->doApiRequestWithToken( [
561 'action' => 'edit',
562 'title' => $name,
563 'undo' => $apiResult['edit']['newrevid']
564 ], null, $sysop )[0];
565
566 // Check success
567 $this->assertArrayHasKey( 'edit', $apiResult );
568 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
569 $this->assertSame( 'Success', $apiResult['edit']['result'] );
570 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
571 // Check that the contentmodel is back to wikitext now.
572 $this->assertSame( 'wikitext', $apiResult['edit']['contentmodel'] );
573 }
574
575 // The tests below are mostly not commented because they do exactly what
576 // you'd expect from the name.
577
578 public function testCorrectContentFormat() {
579 $name = 'Help:' . ucfirst( __FUNCTION__ );
580
581 $this->doApiRequestWithToken( [
582 'action' => 'edit',
583 'title' => $name,
584 'text' => 'some text',
585 'contentmodel' => 'wikitext',
586 'contentformat' => 'text/x-wiki',
587 ] );
588
589 $this->assertTrue( Title::newFromText( $name )->exists() );
590 }
591
593 $name = 'Help:' . ucfirst( __FUNCTION__ );
594
595 $this->setExpectedException( ApiUsageException::class,
596 'Unrecognized value for parameter "contentformat": nonexistent format.' );
597
598 try {
599 $this->doApiRequestWithToken( [
600 'action' => 'edit',
601 'title' => $name,
602 'text' => 'some text',
603 'contentformat' => 'nonexistent format',
604 ] );
605 } finally {
606 $this->assertFalse( Title::newFromText( $name )->exists() );
607 }
608 }
609
610 public function testMismatchedContentFormat() {
611 $name = 'Help:' . ucfirst( __FUNCTION__ );
612
613 $this->setExpectedException( ApiUsageException::class,
614 'The requested format text/plain is not supported for content ' .
615 "model wikitext used by $name." );
616
617 try {
618 $this->doApiRequestWithToken( [
619 'action' => 'edit',
620 'title' => $name,
621 'text' => 'some text',
622 'contentmodel' => 'wikitext',
623 'contentformat' => 'text/plain',
624 ] );
625 } finally {
626 $this->assertFalse( Title::newFromText( $name )->exists() );
627 }
628 }
629
630 public function testUndoToInvalidRev() {
631 $name = 'Help:' . ucfirst( __FUNCTION__ );
632
633 $revId = $this->editPage( $name, 'Some text' )->value['revision']
634 ->getId();
635 $revId++;
636
637 $this->setExpectedException( ApiUsageException::class,
638 "There is no revision with ID $revId." );
639
640 $this->doApiRequestWithToken( [
641 'action' => 'edit',
642 'title' => $name,
643 'undo' => $revId,
644 ] );
645 }
646
652 public function testUndoAfterToInvalidRev() {
653 // We can't just pick a large number for undoafter (as in
654 // testUndoToInvalidRev above), because then MediaWiki will helpfully
655 // assume we switched around undo and undoafter and we'll test the code
656 // path for undo being invalid, not undoafter. So instead we delete
657 // the revision from the database. In real life this case could come
658 // up if a revision number was skipped, e.g., if two transactions try
659 // to insert new revision rows at once and the first one to succeed
660 // gets rolled back.
661 $name = 'Help:' . ucfirst( __FUNCTION__ );
662 $titleObj = Title::newFromText( $name );
663
664 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
665 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
666 $revId3 = $this->editPage( $name, '3' )->value['revision']->getId();
667
668 // Make the middle revision disappear
669 $dbw = wfGetDB( DB_MASTER );
670 $dbw->delete( 'revision', [ 'rev_id' => $revId2 ], __METHOD__ );
671 $dbw->update( 'revision', [ 'rev_parent_id' => $revId1 ],
672 [ 'rev_id' => $revId3 ], __METHOD__ );
673
674 $this->setExpectedException( ApiUsageException::class,
675 "There is no revision with ID $revId2." );
676
677 $this->doApiRequestWithToken( [
678 'action' => 'edit',
679 'title' => $name,
680 'undo' => $revId3,
681 'undoafter' => $revId2,
682 ] );
683 }
684
689 public function testUndoAfterToHiddenRev() {
690 $name = 'Help:' . ucfirst( __FUNCTION__ );
691 $titleObj = Title::newFromText( $name );
692
693 $this->editPage( $name, '0' );
694
695 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
696
697 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
698
699 // Hide the middle revision
700 $list = RevisionDeleter::createList( 'revision',
701 RequestContext::getMain(), $titleObj, [ $revId1 ] );
702 $list->setVisibility( [
703 'value' => [ Revision::DELETED_TEXT => 1 ],
704 'comment' => 'Bye-bye',
705 ] );
706
707 $this->setExpectedException( ApiUsageException::class,
708 "There is no revision with ID $revId1." );
709
710 $this->doApiRequestWithToken( [
711 'action' => 'edit',
712 'title' => $name,
713 'undo' => $revId2,
714 'undoafter' => $revId1,
715 ] );
716 }
717
723 $name = 'Help:' . ucfirst( __FUNCTION__ );
724 $titleObj = Title::newFromText( $name );
725
726 $this->editPage( $name, '0' );
727
728 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
729
730 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
731
732 // Now monkey with the timestamp
733 $dbw = wfGetDB( DB_MASTER );
734 $dbw->update(
735 'revision',
736 [ 'rev_timestamp' => wfTimestamp( TS_MW, time() - 86400 ) ],
737 [ 'rev_id' => $revId1 ],
738 __METHOD__
739 );
740
741 $this->doApiRequestWithToken( [
742 'action' => 'edit',
743 'title' => $name,
744 'undo' => $revId2,
745 'undoafter' => $revId1,
746 ] );
747
748 $text = ( new WikiPage( $titleObj ) )->getContent()->getNativeData();
749
750 // This is wrong! It should be 1. But let's test for our incorrect
751 // behavior for now, so if someone fixes it they'll fix the test as
752 // well to expect 1. If we disabled the test, it might stay disabled
753 // even once the bug is fixed, which would be a shame.
754 $this->assertSame( '2', $text );
755 }
756
757 public function testUndoWithConflicts() {
758 $name = 'Help:' . ucfirst( __FUNCTION__ );
759
760 $this->setExpectedException( ApiUsageException::class,
761 'The edit could not be undone due to conflicting intermediate edits.' );
762
763 $this->editPage( $name, '1' );
764
765 $revId = $this->editPage( $name, '2' )->value['revision']->getId();
766
767 $this->editPage( $name, '3' );
768
769 $this->doApiRequestWithToken( [
770 'action' => 'edit',
771 'title' => $name,
772 'undo' => $revId,
773 ] );
774
775 $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
776 ->getNativeData();
777 $this->assertSame( '3', $text );
778 }
779
784 public function testReversedUndoAfter() {
785 $name = 'Help:' . ucfirst( __FUNCTION__ );
786
787 $this->editPage( $name, '0' );
788 $revId1 = $this->editPage( $name, '1' )->value['revision']->getId();
789 $revId2 = $this->editPage( $name, '2' )->value['revision']->getId();
790
791 $this->doApiRequestWithToken( [
792 'action' => 'edit',
793 'title' => $name,
794 'undo' => $revId1,
795 'undoafter' => $revId2,
796 ] );
797
798 $text = ( new WikiPage( Title::newFromText( $name ) ) )->getContent()
799 ->getNativeData();
800 $this->assertSame( '1', $text );
801 }
802
804 $name = 'Help:' . ucfirst( __FUNCTION__ );
805
806 $this->editPage( "$name-1", 'Some text' );
807 $revId = $this->editPage( "$name-1", 'Some more text' )
808 ->value['revision']->getId();
809
810 $this->editPage( "$name-2", 'Some text' );
811
812 $this->setExpectedException( ApiUsageException::class,
813 "r$revId is not a revision of $name-2." );
814
815 $this->doApiRequestWithToken( [
816 'action' => 'edit',
817 'title' => "$name-2",
818 'undo' => $revId,
819 ] );
820 }
821
823 $name = 'Help:' . ucfirst( __FUNCTION__ );
824
825 $revId1 = $this->editPage( "$name-1", 'Some text' )
826 ->value['revision']->getId();
827
828 $revId2 = $this->editPage( "$name-2", 'Some text' )
829 ->value['revision']->getId();
830
831 $this->setExpectedException( ApiUsageException::class,
832 "r$revId1 is not a revision of $name-2." );
833
834 $this->doApiRequestWithToken( [
835 'action' => 'edit',
836 'title' => "$name-2",
837 'undo' => $revId2,
838 'undoafter' => $revId1,
839 ] );
840 }
841
842 public function testMd5Text() {
843 $name = 'Help:' . ucfirst( __FUNCTION__ );
844
845 $this->assertFalse( Title::newFromText( $name )->exists() );
846
847 $this->doApiRequestWithToken( [
848 'action' => 'edit',
849 'title' => $name,
850 'text' => 'Some text',
851 'md5' => md5( 'Some text' ),
852 ] );
853
854 $this->assertTrue( Title::newFromText( $name )->exists() );
855 }
856
857 public function testMd5PrependText() {
858 $name = 'Help:' . ucfirst( __FUNCTION__ );
859
860 $this->editPage( $name, 'Some text' );
861
862 $this->doApiRequestWithToken( [
863 'action' => 'edit',
864 'title' => $name,
865 'prependtext' => 'Alert: ',
866 'md5' => md5( 'Alert: ' ),
867 ] );
868
869 $text = ( new WikiPage( Title::newFromText( $name ) ) )
870 ->getContent()->getNativeData();
871 $this->assertSame( 'Alert: Some text', $text );
872 }
873
874 public function testMd5AppendText() {
875 $name = 'Help:' . ucfirst( __FUNCTION__ );
876
877 $this->editPage( $name, 'Some text' );
878
879 $this->doApiRequestWithToken( [
880 'action' => 'edit',
881 'title' => $name,
882 'appendtext' => ' is nice',
883 'md5' => md5( ' is nice' ),
884 ] );
885
886 $text = ( new WikiPage( Title::newFromText( $name ) ) )
887 ->getContent()->getNativeData();
888 $this->assertSame( 'Some text is nice', $text );
889 }
890
891 public function testMd5PrependAndAppendText() {
892 $name = 'Help:' . ucfirst( __FUNCTION__ );
893
894 $this->editPage( $name, 'Some text' );
895
896 $this->doApiRequestWithToken( [
897 'action' => 'edit',
898 'title' => $name,
899 'prependtext' => 'Alert: ',
900 'appendtext' => ' is nice',
901 'md5' => md5( 'Alert: is nice' ),
902 ] );
903
904 $text = ( new WikiPage( Title::newFromText( $name ) ) )
905 ->getContent()->getNativeData();
906 $this->assertSame( 'Alert: Some text is nice', $text );
907 }
908
909 public function testIncorrectMd5Text() {
910 $name = 'Help:' . ucfirst( __FUNCTION__ );
911
912 $this->setExpectedException( ApiUsageException::class,
913 'The supplied MD5 hash was incorrect.' );
914
915 $this->doApiRequestWithToken( [
916 'action' => 'edit',
917 'title' => $name,
918 'text' => 'Some text',
919 'md5' => md5( '' ),
920 ] );
921 }
922
923 public function testIncorrectMd5PrependText() {
924 $name = 'Help:' . ucfirst( __FUNCTION__ );
925
926 $this->setExpectedException( ApiUsageException::class,
927 'The supplied MD5 hash was incorrect.' );
928
929 $this->doApiRequestWithToken( [
930 'action' => 'edit',
931 'title' => $name,
932 'prependtext' => 'Some ',
933 'appendtext' => 'text',
934 'md5' => md5( 'Some ' ),
935 ] );
936 }
937
938 public function testIncorrectMd5AppendText() {
939 $name = 'Help:' . ucfirst( __FUNCTION__ );
940
941 $this->setExpectedException( ApiUsageException::class,
942 'The supplied MD5 hash was incorrect.' );
943
944 $this->doApiRequestWithToken( [
945 'action' => 'edit',
946 'title' => $name,
947 'prependtext' => 'Some ',
948 'appendtext' => 'text',
949 'md5' => md5( 'text' ),
950 ] );
951 }
952
953 public function testCreateOnly() {
954 $name = 'Help:' . ucfirst( __FUNCTION__ );
955
956 $this->setExpectedException( ApiUsageException::class,
957 'The article you tried to create has been created already.' );
958
959 $this->editPage( $name, 'Some text' );
960 $this->assertTrue( Title::newFromText( $name )->exists() );
961
962 try {
963 $this->doApiRequestWithToken( [
964 'action' => 'edit',
965 'title' => $name,
966 'text' => 'Some more text',
967 'createonly' => '',
968 ] );
969 } finally {
970 // Validate that content was not changed
971 $text = ( new WikiPage( Title::newFromText( $name ) ) )
972 ->getContent()->getNativeData();
973
974 $this->assertSame( 'Some text', $text );
975 }
976 }
977
978 public function testNoCreate() {
979 $name = 'Help:' . ucfirst( __FUNCTION__ );
980
981 $this->setExpectedException( ApiUsageException::class,
982 "The page you specified doesn't exist." );
983
984 $this->assertFalse( Title::newFromText( $name )->exists() );
985
986 try {
987 $this->doApiRequestWithToken( [
988 'action' => 'edit',
989 'title' => $name,
990 'text' => 'Some text',
991 'nocreate' => '',
992 ] );
993 } finally {
994 $this->assertFalse( Title::newFromText( $name )->exists() );
995 }
996 }
997
1004 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1005
1006 $this->setExpectedException( ApiUsageException::class,
1007 "Can't append to pages using content model testing-nontext." );
1008
1009 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1010 function ( Title $title, &$model ) use ( $name ) {
1011 if ( $title->getPrefixedText() === $name ) {
1012 $model = 'testing-nontext';
1013 }
1014 return true;
1015 }
1016 );
1017
1018 $this->doApiRequestWithToken( [
1019 'action' => 'edit',
1020 'title' => $name,
1021 'appendtext' => 'Some text',
1022 ] );
1023 }
1024
1026 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1027
1028 $this->assertFalse( Title::newFromText( $name )->exists() );
1029
1030 $this->doApiRequestWithToken( [
1031 'action' => 'edit',
1032 'title' => $name,
1033 'appendtext' => 'Some text',
1034 ] );
1035
1036 $this->assertTrue( Title::newFromText( $name )->exists() );
1037 }
1038
1040 $name = 'MediaWiki:' . ucfirst( __FUNCTION__ );
1041
1042 $this->setExpectedException( ApiUsageException::class,
1043 'Content serialization failed: Could not unserialize content' );
1044
1045 $this->setTemporaryHook( 'ContentHandlerDefaultModelFor',
1046 function ( Title $title, &$model ) use ( $name ) {
1047 if ( $title->getPrefixedText() === $name ) {
1048 $model = 'testing-serialize-error';
1049 }
1050 return true;
1051 }
1052 );
1053
1054 $this->doApiRequestWithToken( [
1055 'action' => 'edit',
1056 'title' => $name,
1057 'appendtext' => 'Some text',
1058 ] );
1059 }
1060
1061 public function testAppendNewSection() {
1062 $name = 'Help:' . ucfirst( __FUNCTION__ );
1063
1064 $this->editPage( $name, 'Initial content' );
1065
1066 $this->doApiRequestWithToken( [
1067 'action' => 'edit',
1068 'title' => $name,
1069 'appendtext' => '== New section ==',
1070 'section' => 'new',
1071 ] );
1072
1073 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1074 ->getContent()->getNativeData();
1075
1076 $this->assertSame( "Initial content\n\n== New section ==", $text );
1077 }
1078
1080 $name = 'Help:' . ucfirst( __FUNCTION__ );
1081
1082 $this->setExpectedException( ApiUsageException::class,
1083 'Sections are not supported for content model text.' );
1084
1085 $this->editPage( $name, 'Initial content' );
1086
1087 $this->doApiRequestWithToken( [
1088 'action' => 'edit',
1089 'title' => $name,
1090 'appendtext' => '== New section ==',
1091 'section' => 'new',
1092 'contentmodel' => 'text',
1093 ] );
1094 }
1095
1097 $name = 'Help:' . ucfirst( __FUNCTION__ );
1098
1099 $this->editPage( $name, 'Initial content' );
1100
1101 $this->doApiRequestWithToken( [
1102 'action' => 'edit',
1103 'title' => $name,
1104 'sectiontitle' => 'My section',
1105 'appendtext' => 'More content',
1106 'section' => 'new',
1107 ] );
1108
1109 $page = new WikiPage( Title::newFromText( $name ) );
1110
1111 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1112 $page->getContent()->getNativeData() );
1113 $this->assertSame( '/* My section */ new section',
1114 $page->getRevision()->getComment() );
1115 }
1116
1118 $name = 'Help:' . ucfirst( __FUNCTION__ );
1119
1120 $this->editPage( $name, 'Initial content' );
1121
1122 $this->doApiRequestWithToken( [
1123 'action' => 'edit',
1124 'title' => $name,
1125 'appendtext' => 'More content',
1126 'section' => 'new',
1127 'summary' => 'Add new section',
1128 ] );
1129
1130 $page = new WikiPage( Title::newFromText( $name ) );
1131
1132 $this->assertSame( "Initial content\n\n== Add new section ==\n\nMore content",
1133 $page->getContent()->getNativeData() );
1134 // EditPage actually assumes the summary is the section name here
1135 $this->assertSame( '/* Add new section */ new section',
1136 $page->getRevision()->getComment() );
1137 }
1138
1140 $name = 'Help:' . ucfirst( __FUNCTION__ );
1141
1142 $this->editPage( $name, 'Initial content' );
1143
1144 $this->doApiRequestWithToken( [
1145 'action' => 'edit',
1146 'title' => $name,
1147 'sectiontitle' => 'My section',
1148 'appendtext' => 'More content',
1149 'section' => 'new',
1150 'summary' => 'Add new section',
1151 ] );
1152
1153 $page = new WikiPage( Title::newFromText( $name ) );
1154
1155 $this->assertSame( "Initial content\n\n== My section ==\n\nMore content",
1156 $page->getContent()->getNativeData() );
1157 $this->assertSame( 'Add new section',
1158 $page->getRevision()->getComment() );
1159 }
1160
1161 public function testAppendToSection() {
1162 $name = 'Help:' . ucfirst( __FUNCTION__ );
1163
1164 $this->editPage( $name, "== Section 1 ==\n\nContent\n\n" .
1165 "== Section 2 ==\n\nFascinating!" );
1166
1167 $this->doApiRequestWithToken( [
1168 'action' => 'edit',
1169 'title' => $name,
1170 'appendtext' => ' and more content',
1171 'section' => '1',
1172 ] );
1173
1174 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1175 ->getContent()->getNativeData();
1176
1177 $this->assertSame( "== Section 1 ==\n\nContent and more content\n\n" .
1178 "== Section 2 ==\n\nFascinating!", $text );
1179 }
1180
1181 public function testAppendToFirstSection() {
1182 $name = 'Help:' . ucfirst( __FUNCTION__ );
1183
1184 $this->editPage( $name, "Content\n\n== Section 1 ==\n\nFascinating!" );
1185
1186 $this->doApiRequestWithToken( [
1187 'action' => 'edit',
1188 'title' => $name,
1189 'appendtext' => ' and more content',
1190 'section' => '0',
1191 ] );
1192
1193 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1194 ->getContent()->getNativeData();
1195
1196 $this->assertSame( "Content and more content\n\n== Section 1 ==\n\n" .
1197 "Fascinating!", $text );
1198 }
1199
1201 $name = 'Help:' . ucfirst( __FUNCTION__ );
1202
1203 $this->setExpectedException( ApiUsageException::class, 'There is no section 1.' );
1204
1205 $this->editPage( $name, 'Content' );
1206
1207 try {
1208 $this->doApiRequestWithToken( [
1209 'action' => 'edit',
1210 'title' => $name,
1211 'appendtext' => ' and more content',
1212 'section' => '1',
1213 ] );
1214 } finally {
1215 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1216 ->getContent()->getNativeData();
1217
1218 $this->assertSame( 'Content', $text );
1219 }
1220 }
1221
1222 public function testEditMalformedSection() {
1223 $name = 'Help:' . ucfirst( __FUNCTION__ );
1224
1225 $this->setExpectedException( ApiUsageException::class,
1226 'The "section" parameter must be a valid section ID or "new".' );
1227 $this->editPage( $name, 'Content' );
1228
1229 try {
1230 $this->doApiRequestWithToken( [
1231 'action' => 'edit',
1232 'title' => $name,
1233 'text' => 'Different content',
1234 'section' => 'It is unlikely that this is valid',
1235 ] );
1236 } finally {
1237 $text = ( new WikiPage( Title::newFromText( $name ) ) )
1238 ->getContent()->getNativeData();
1239
1240 $this->assertSame( 'Content', $text );
1241 }
1242 }
1243
1244 public function testEditWithStartTimestamp() {
1245 $name = 'Help:' . ucfirst( __FUNCTION__ );
1246 $this->setExpectedException( ApiUsageException::class,
1247 'The page has been deleted since you fetched its timestamp.' );
1248
1249 $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
1250
1251 $this->editPage( $name, 'Some text' );
1252
1253 $pageObj = new WikiPage( Title::newFromText( $name ) );
1254 $pageObj->doDeleteArticle( 'Bye-bye' );
1255
1256 $this->assertFalse( $pageObj->exists() );
1257
1258 try {
1259 $this->doApiRequestWithToken( [
1260 'action' => 'edit',
1261 'title' => $name,
1262 'text' => 'Different text',
1263 'starttimestamp' => $startTime,
1264 ] );
1265 } finally {
1266 $this->assertFalse( $pageObj->exists() );
1267 }
1268 }
1269
1270 public function testEditMinor() {
1271 $name = 'Help:' . ucfirst( __FUNCTION__ );
1272
1273 $this->editPage( $name, 'Some text' );
1274
1275 $this->doApiRequestWithToken( [
1276 'action' => 'edit',
1277 'title' => $name,
1278 'text' => 'Different text',
1279 'minor' => '',
1280 ] );
1281
1282 $revisionStore = \MediaWiki\MediaWikiServices::getInstance()->getRevisionStore();
1283 $revision = $revisionStore->getRevisionByTitle( Title::newFromText( $name ) );
1284 $this->assertTrue( $revision->isMinor() );
1285 }
1286
1287 public function testEditRecreate() {
1288 $name = 'Help:' . ucfirst( __FUNCTION__ );
1289
1290 $startTime = MWTimestamp::convert( TS_MW, time() - 1 );
1291
1292 $this->editPage( $name, 'Some text' );
1293
1294 $pageObj = new WikiPage( Title::newFromText( $name ) );
1295 $pageObj->doDeleteArticle( 'Bye-bye' );
1296
1297 $this->assertFalse( $pageObj->exists() );
1298
1299 $this->doApiRequestWithToken( [
1300 'action' => 'edit',
1301 'title' => $name,
1302 'text' => 'Different text',
1303 'starttimestamp' => $startTime,
1304 'recreate' => '',
1305 ] );
1306
1307 $this->assertTrue( Title::newFromText( $name )->exists() );
1308 }
1309
1310 public function testEditWatch() {
1311 $name = 'Help:' . ucfirst( __FUNCTION__ );
1312 $user = self::$users['sysop']->getUser();
1313
1314 $this->doApiRequestWithToken( [
1315 'action' => 'edit',
1316 'title' => $name,
1317 'text' => 'Some text',
1318 'watch' => '',
1319 ] );
1320
1321 $this->assertTrue( Title::newFromText( $name )->exists() );
1322 $this->assertTrue( $user->isWatched( Title::newFromText( $name ) ) );
1323 }
1324
1325 public function testEditUnwatch() {
1326 $name = 'Help:' . ucfirst( __FUNCTION__ );
1327 $user = self::$users['sysop']->getUser();
1328 $titleObj = Title::newFromText( $name );
1329
1330 $user->addWatch( $titleObj );
1331
1332 $this->assertFalse( $titleObj->exists() );
1333 $this->assertTrue( $user->isWatched( $titleObj ) );
1334
1335 $this->doApiRequestWithToken( [
1336 'action' => 'edit',
1337 'title' => $name,
1338 'text' => 'Some text',
1339 'unwatch' => '',
1340 ] );
1341
1342 $this->assertTrue( $titleObj->exists() );
1343 $this->assertFalse( $user->isWatched( $titleObj ) );
1344 }
1345
1346 public function testEditWithTag() {
1347 $name = 'Help:' . ucfirst( __FUNCTION__ );
1348
1349 ChangeTags::defineTag( 'custom tag' );
1350
1351 $revId = $this->doApiRequestWithToken( [
1352 'action' => 'edit',
1353 'title' => $name,
1354 'text' => 'Some text',
1355 'tags' => 'custom tag',
1356 ] )[0]['edit']['newrevid'];
1357
1358 $dbw = wfGetDB( DB_MASTER );
1359 $this->assertSame( 'custom tag', $dbw->selectField(
1360 'change_tag', 'ct_tag', [ 'ct_rev_id' => $revId ], __METHOD__ ) );
1361 }
1362
1364 $name = 'Help:' . ucfirst( __FUNCTION__ );
1365
1366 $this->setExpectedException( ApiUsageException::class,
1367 'You do not have permission to apply change tags along with your changes.' );
1368
1369 $this->assertFalse( Title::newFromText( $name )->exists() );
1370
1371 ChangeTags::defineTag( 'custom tag' );
1372 $this->setMwGlobals( 'wgRevokePermissions',
1373 [ 'user' => [ 'applychangetags' => true ] ] );
1374 try {
1375 $this->doApiRequestWithToken( [
1376 'action' => 'edit',
1377 'title' => $name,
1378 'text' => 'Some text',
1379 'tags' => 'custom tag',
1380 ] );
1381 } finally {
1382 $this->assertFalse( Title::newFromText( $name )->exists() );
1383 }
1384 }
1385
1386 public function testEditAbortedByHook() {
1387 $name = 'Help:' . ucfirst( __FUNCTION__ );
1388
1389 $this->setExpectedException( ApiUsageException::class,
1390 'The modification you tried to make was aborted by an extension.' );
1391
1392 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1393 'hook-APIEditBeforeSave-closure)' );
1394
1395 $this->setTemporaryHook( 'APIEditBeforeSave',
1396 function () {
1397 return false;
1398 }
1399 );
1400
1401 try {
1402 $this->doApiRequestWithToken( [
1403 'action' => 'edit',
1404 'title' => $name,
1405 'text' => 'Some text',
1406 ] );
1407 } finally {
1408 $this->assertFalse( Title::newFromText( $name )->exists() );
1409 }
1410 }
1411
1413 $name = 'Help:' . ucfirst( __FUNCTION__ );
1414
1415 $this->hideDeprecated( 'APIEditBeforeSave hook (used in ' .
1416 'hook-APIEditBeforeSave-closure)' );
1417
1418 $this->setTemporaryHook( 'APIEditBeforeSave',
1419 function ( $unused1, $unused2, &$r ) {
1420 $r['msg'] = 'Some message';
1421 return false;
1422 } );
1423
1424 $result = $this->doApiRequestWithToken( [
1425 'action' => 'edit',
1426 'title' => $name,
1427 'text' => 'Some text',
1428 ] );
1429 Wikimedia\restoreWarnings();
1430
1431 $this->assertSame( [ 'msg' => 'Some message', 'result' => 'Failure' ],
1432 $result[0]['edit'] );
1433
1434 $this->assertFalse( Title::newFromText( $name )->exists() );
1435 }
1436
1438 $name = 'Help:' . ucfirst( __FUNCTION__ );
1439
1440 $this->setTemporaryHook( 'EditFilterMergedContent',
1441 function ( $unused1, $unused2, Status $status ) {
1442 $status->apiHookResult = [ 'msg' => 'A message for you!' ];
1443 return false;
1444 } );
1445
1446 $res = $this->doApiRequestWithToken( [
1447 'action' => 'edit',
1448 'title' => $name,
1449 'text' => 'Some text',
1450 ] );
1451
1452 $this->assertFalse( Title::newFromText( $name )->exists() );
1453 $this->assertSame( [ 'edit' => [ 'msg' => 'A message for you!',
1454 'result' => 'Failure' ] ], $res[0] );
1455 }
1456
1458 $name = 'Help:' . ucfirst( __FUNCTION__ );
1459
1460 $this->setExpectedException( ApiUsageException::class,
1461 'The modification you tried to make was aborted by an extension.' );
1462
1463 $this->setTemporaryHook( 'EditFilterMergedContent',
1464 function () {
1465 return false;
1466 }
1467 );
1468
1469 try {
1470 $this->doApiRequestWithToken( [
1471 'action' => 'edit',
1472 'title' => $name,
1473 'text' => 'Some text',
1474 ] );
1475 } finally {
1476 $this->assertFalse( Title::newFromText( $name )->exists() );
1477 }
1478 }
1479
1480 public function testEditWhileBlocked() {
1481 $name = 'Help:' . ucfirst( __FUNCTION__ );
1482
1483 $this->setExpectedException( ApiUsageException::class,
1484 'You have been blocked from editing.' );
1485
1486 $block = new Block( [
1487 'address' => self::$users['sysop']->getUser()->getName(),
1488 'by' => self::$users['sysop']->getUser()->getId(),
1489 'reason' => 'Capriciousness',
1490 'timestamp' => '19370101000000',
1491 'expiry' => 'infinity',
1492 ] );
1493 $block->insert();
1494
1495 try {
1496 $this->doApiRequestWithToken( [
1497 'action' => 'edit',
1498 'title' => $name,
1499 'text' => 'Some text',
1500 ] );
1501 } finally {
1502 $block->delete();
1503 self::$users['sysop']->getUser()->clearInstanceCache();
1504 }
1505 }
1506
1507 public function testEditWhileReadOnly() {
1508 $name = 'Help:' . ucfirst( __FUNCTION__ );
1509
1510 $this->setExpectedException( ApiUsageException::class,
1511 'The wiki is currently in read-only mode.' );
1512
1513 $svc = \MediaWiki\MediaWikiServices::getInstance()->getReadOnlyMode();
1514 $svc->setReason( "Read-only for testing" );
1515
1516 try {
1517 $this->doApiRequestWithToken( [
1518 'action' => 'edit',
1519 'title' => $name,
1520 'text' => 'Some text',
1521 ] );
1522 } finally {
1523 $svc->setReason( false );
1524 }
1525 }
1526
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
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
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
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}
serialize()
$wgExtraNamespaces
Additional namespaces.
$wgNamespaceContentModels
Associative array mapping namespace IDs to the name of the content model pages in that namespace shou...
$wgContentHandlers
Plugins for page content model handling.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Tests for MediaWiki api.php?action=edit.
testEditSection()
Test editing of sections.
testAppendNewSectionWithInvalidContentModel()
testAppendInMediaWikiNamespaceWithSerializationError()
testEditAbortedByEditPageHookWithNoResult()
testUndoAfterContentModelChange()
This test verifies that after changing the content model of a page, undoing that edit via the API wil...
testCheckDirectApiEditingDisallowed_forNonTextContent()
testEdit_redirectText()
Ensure we cannot edit through a redirect, if attempting to overwrite content.
testEditNewSection()
Test action=edit&section=new Run it twice so we test adding a new section on a page that doesn't exis...
testAppendWithNonTextContentHandler()
Appending/prepending is currently only supported for TextContent.
testUndoWithSwappedRevisions()
Test undo when a revision with a higher id has an earlier timestamp.
testEditConflict_newSection()
Ensure that editing using section=new will prevent simple conflicts.
testReversedUndoAfter()
undoafter is supposed to be less than undo.
testEdit_redirect()
Ensure we can edit through a redirect, if adding a section.
testEditAppend( $text, $op, $append, $expected)
provideEditAppend
testUndoAfterToHiddenRev()
Tests what happens if the undo parameter is a valid revision, but undoafter is hidden (rev_deleted).
testSupportsDirectApiEditing_withContentHandlerOverride()
forceRevisionDate(WikiPage $page, $timestamp)
testUndoAfterToInvalidRev()
Tests what happens if the undo parameter is a valid revision, but the undoafter parameter doesn't ref...
Exception used to abort API execution with an error.
static defineTag( $tag)
Defines a tag in the valid_tag table, without checking that the tag name is valid.
setMwGlobals( $pairs, $value=null)
Sets a global, maintaining a stashed version of the previous global to be restored in tearDown.
static getMain()
Get the RequestContext object associated with the main request.
static createList( $typeName, IContextSource $context, Title $title, array $ids)
Instantiate the appropriate list class for a given list of IDs.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:40
Represents a title within MediaWiki.
Definition Title.php:39
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:53
Class representing a MediaWiki article and history.
Definition WikiPage.php:37
getLatest()
Get the page_latest field.
Definition WikiPage.php:624
clear()
Clear the object.
Definition WikiPage.php:244
Content object for wiki text pages.
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
when a variable name is used in a function
Definition design.txt:94
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1051
returning false will NOT prevent logging $e
Definition hooks.txt:2176
const EDIT_UPDATE
Definition Defines.php:163
const EDIT_NEW
Definition Defines.php:162
you have access to all of the normal MediaWiki so you can get a DB use the cache
const DB_MASTER
Definition defines.php:29