Merge "Revert "Log the reason why revision->getContent() returns null""
[lhc/web/wiklou.git] / tests / phpunit / includes / api / ApiEditPageTest.php
1 <?php
2
3 /**
4 * Tests for MediaWiki api.php?action=edit.
5 *
6 * @author Daniel Kinzler
7 *
8 * @group API
9 * @group Database
10 * @group medium
11 *
12 * @covers ApiEditPage
13 */
14 class ApiEditPageTest extends ApiTestCase {
15
16 protected function setUp() {
17 global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
18
19 parent::setUp();
20
21 $this->setMwGlobals( [
22 'wgExtraNamespaces' => $wgExtraNamespaces,
23 'wgNamespaceContentModels' => $wgNamespaceContentModels,
24 'wgContentHandlers' => $wgContentHandlers,
25 'wgContLang' => $wgContLang,
26 ] );
27
28 $wgExtraNamespaces[12312] = 'Dummy';
29 $wgExtraNamespaces[12313] = 'Dummy_talk';
30 $wgExtraNamespaces[12314] = 'DummyNonText';
31 $wgExtraNamespaces[12315] = 'DummyNonText_talk';
32
33 $wgNamespaceContentModels[12312] = "testing";
34 $wgNamespaceContentModels[12314] = "testing-nontext";
35
36 $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
37 $wgContentHandlers["testing-nontext"] = 'DummyNonTextContentHandler';
38
39 MWNamespace::clearCaches();
40 $wgContLang->resetNamespaces(); # reset namespace cache
41
42 $this->doLogin();
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->assertEquals( '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->assertEquals( '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->assertEquals( '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
108 /**
109 * @return array
110 */
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
134 /**
135 * @dataProvider provideEditAppend
136 */
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->assertEquals( '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->assertEquals( '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->assertEquals( $expected, $text );
170 }
171
172 /**
173 * Test editing of sections
174 */
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->assertEquals( 'Success', $re['edit']['result'] );
189 $newtext = WikiPage::factory( Title::newFromText( $name ) )
190 ->getContent( Revision::RAW )
191 ->getNativeData();
192 $this->assertEquals( "==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
208 /**
209 * Test action=edit&section=new
210 * Run it twice so we test adding a new section on a
211 * page that doesn't exist (T54830) and one that
212 * does exist
213 */
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->assertEquals( '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->assertEquals( "== 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->assertEquals( 'Success', $re2['edit']['result'] );
245 $text = WikiPage::factory( Title::newFromText( $name ) )
246 ->getContent( Revision::RAW )
247 ->getNativeData();
248 $this->assertEquals( "== header ==\n\ntest\n\n== header ==\n\ntest", $text );
249 }
250
251 /**
252 * Ensure we can edit through a redirect, if adding a section
253 */
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 ], null, self::$users['sysop']->getUser() );
292
293 $this->assertEquals( 'Success', $re['edit']['result'],
294 "no problems expected when following redirect" );
295 }
296
297 /**
298 * Ensure we cannot edit through a redirect, if attempting to overwrite content
299 */
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 ], null, self::$users['sysop']->getUser() );
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 ], null, self::$users['sysop']->getUser() );
374
375 $this->fail( 'edit conflict expected' );
376 } catch ( ApiUsageException $ex ) {
377 $this->assertTrue( self::apiExceptionHasCode( $ex, 'editconflict' ) );
378 }
379 }
380
381 /**
382 * Ensure that editing using section=new will prevent simple conflicts
383 */
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 ], null, self::$users['sysop']->getUser() );
413
414 $this->assertEquals( '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 ], null, self::$users['sysop']->getUser() );
460
461 $this->assertEquals( 'Success', $re['edit']['result'],
462 "no edit conflict expected here" );
463 }
464
465 /**
466 * @param WikiPage $page
467 * @param string|int $timestamp
468 */
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
479 public function testCheckDirectApiEditingDisallowed_forNonTextContent() {
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
493 public function testSupportsDirectApiEditing_withContentHandlerOverride() {
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->assertEquals( '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->assertEquals( "testing-nontext", $page->getContentModel() );
518 $this->assertEquals( $data, $page->getContent()->serialize() );
519 }
520
521 /**
522 * This test verifies that after changing the content model
523 * of a page, undoing that edit via the API will also
524 * undo the content model change.
525 */
526 public function testUndoAfterContentModelChange() {
527 $name = 'Help:' . __FUNCTION__;
528 $uploader = self::$users['uploader']->getUser();
529 $sysop = self::$users['sysop']->getUser();
530 $apiResult = $this->doApiRequestWithToken( [
531 'action' => 'edit',
532 'title' => $name,
533 'text' => 'some text',
534 ], null, $sysop )[0];
535
536 // Check success
537 $this->assertArrayHasKey( 'edit', $apiResult );
538 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
539 $this->assertEquals( 'Success', $apiResult['edit']['result'] );
540 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
541 // Content model is wikitext
542 $this->assertEquals( 'wikitext', $apiResult['edit']['contentmodel'] );
543
544 // Convert the page to JSON
545 $apiResult = $this->doApiRequestWithToken( [
546 'action' => 'edit',
547 'title' => $name,
548 'text' => '{}',
549 'contentmodel' => 'json',
550 ], null, $uploader )[0];
551
552 // Check success
553 $this->assertArrayHasKey( 'edit', $apiResult );
554 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
555 $this->assertEquals( 'Success', $apiResult['edit']['result'] );
556 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
557 $this->assertEquals( 'json', $apiResult['edit']['contentmodel'] );
558
559 $apiResult = $this->doApiRequestWithToken( [
560 'action' => 'edit',
561 'title' => $name,
562 'undo' => $apiResult['edit']['newrevid']
563 ], null, $sysop )[0];
564
565 // Check success
566 $this->assertArrayHasKey( 'edit', $apiResult );
567 $this->assertArrayHasKey( 'result', $apiResult['edit'] );
568 $this->assertEquals( 'Success', $apiResult['edit']['result'] );
569 $this->assertArrayHasKey( 'contentmodel', $apiResult['edit'] );
570 // Check that the contentmodel is back to wikitext now.
571 $this->assertEquals( 'wikitext', $apiResult['edit']['contentmodel'] );
572 }
573 }