Skin: Make skins aware of their registered skin name
[lhc/web/wiklou.git] / tests / phpunit / includes / EditPageTest.php
1 <?php
2
3 /**
4 * @group Editing
5 *
6 * @group Database
7 * ^--- tell jenkins this test needs the database
8 *
9 * @group medium
10 * ^--- tell phpunit that these test cases may take longer than 2 seconds.
11 */
12 class EditPageTest extends MediaWikiLangTestCase {
13
14 protected function setUp() {
15 global $wgExtraNamespaces, $wgNamespaceContentModels, $wgContentHandlers, $wgContLang;
16
17 parent::setUp();
18
19 $this->setMwGlobals( [
20 'wgExtraNamespaces' => $wgExtraNamespaces,
21 'wgNamespaceContentModels' => $wgNamespaceContentModels,
22 'wgContentHandlers' => $wgContentHandlers,
23 'wgContLang' => $wgContLang,
24 ] );
25
26 $wgExtraNamespaces[12312] = 'Dummy';
27 $wgExtraNamespaces[12313] = 'Dummy_talk';
28
29 $wgNamespaceContentModels[12312] = "testing";
30 $wgContentHandlers["testing"] = 'DummyContentHandlerForTesting';
31
32 MWNamespace::getCanonicalNamespaces( true ); # reset namespace cache
33 $wgContLang->resetNamespaces(); # reset namespace cache
34 }
35
36 /**
37 * @dataProvider provideExtractSectionTitle
38 * @covers EditPage::extractSectionTitle
39 */
40 public function testExtractSectionTitle( $section, $title ) {
41 $extracted = EditPage::extractSectionTitle( $section );
42 $this->assertEquals( $title, $extracted );
43 }
44
45 public static function provideExtractSectionTitle() {
46 return [
47 [
48 "== Test ==\n\nJust a test section.",
49 "Test"
50 ],
51 [
52 "An initial section, no header.",
53 false
54 ],
55 [
56 "An initial section with a fake heder (T34617)\n\n== Test == ??\nwtf",
57 false
58 ],
59 [
60 "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo",
61 "Section"
62 ],
63 [
64 "== Section== \t\r\n followed by whitespace (T37051)",
65 'Section',
66 ],
67 ];
68 }
69
70 protected function forceRevisionDate( WikiPage $page, $timestamp ) {
71 $dbw = wfGetDB( DB_MASTER );
72
73 $dbw->update( 'revision',
74 [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
75 [ 'rev_id' => $page->getLatest() ] );
76
77 $page->clear();
78 }
79
80 /**
81 * User input text is passed to rtrim() by edit page. This is a simple
82 * wrapper around assertEquals() which calls rrtrim() to normalize the
83 * expected and actual texts.
84 * @param string $expected
85 * @param string $actual
86 * @param string $msg
87 */
88 protected function assertEditedTextEquals( $expected, $actual, $msg = '' ) {
89 $this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg );
90 }
91
92 /**
93 * Performs an edit and checks the result.
94 *
95 * @param string|Title $title The title of the page to edit
96 * @param string|null $baseText Some text to create the page with before attempting the edit.
97 * @param User|string|null $user The user to perform the edit as.
98 * @param array $edit An array of request parameters used to define the edit to perform.
99 * Some well known fields are:
100 * * wpTextbox1: the text to submit
101 * * wpSummary: the edit summary
102 * * wpEditToken: the edit token (will be inserted if not provided)
103 * * wpEdittime: timestamp of the edit's base revision (will be inserted
104 * if not provided)
105 * * wpStarttime: timestamp when the edit started (will be inserted if not provided)
106 * * wpSectionTitle: the section to edit
107 * * wpMinorEdit: mark as minor edit
108 * * wpWatchthis: whether to watch the page
109 * @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants).
110 * Set to null to skip the check.
111 * @param string|null $expectedText The text expected to be on the page after the edit.
112 * Set to null to skip the check.
113 * @param string|null $message An optional message to show along with any error message.
114 *
115 * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
116 */
117 protected function assertEdit( $title, $baseText, $user = null, array $edit,
118 $expectedCode = null, $expectedText = null, $message = null
119 ) {
120 if ( is_string( $title ) ) {
121 $ns = $this->getDefaultWikitextNS();
122 $title = Title::newFromText( $title, $ns );
123 }
124 $this->assertNotNull( $title );
125
126 if ( is_string( $user ) ) {
127 $user = User::newFromName( $user );
128
129 if ( $user->getId() === 0 ) {
130 $user->addToDatabase();
131 }
132 }
133
134 $page = WikiPage::factory( $title );
135
136 if ( $baseText !== null ) {
137 $content = ContentHandler::makeContent( $baseText, $title );
138 $page->doEditContent( $content, "base text for test" );
139 $this->forceRevisionDate( $page, '20120101000000' );
140
141 // sanity check
142 $page->clear();
143 $currentText = ContentHandler::getContentText( $page->getContent() );
144
145 # EditPage rtrim() the user input, so we alter our expected text
146 # to reflect that.
147 $this->assertEditedTextEquals( $baseText, $currentText );
148 }
149
150 if ( $user == null ) {
151 $user = $GLOBALS['wgUser'];
152 } else {
153 $this->setMwGlobals( 'wgUser', $user );
154 }
155
156 if ( !isset( $edit['wpEditToken'] ) ) {
157 $edit['wpEditToken'] = $user->getEditToken();
158 }
159
160 if ( !isset( $edit['wpEdittime'] ) ) {
161 $edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : '';
162 }
163
164 if ( !isset( $edit['wpStarttime'] ) ) {
165 $edit['wpStarttime'] = wfTimestampNow();
166 }
167
168 $req = new FauxRequest( $edit, true ); // session ??
169
170 $article = new Article( $title );
171 $article->getContext()->setTitle( $title );
172 $ep = new EditPage( $article );
173 $ep->setContextTitle( $title );
174 $ep->importFormData( $req );
175
176 $bot = isset( $edit['bot'] ) ? (bool)$edit['bot'] : false;
177
178 // this is where the edit happens!
179 // Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut
180 // and throws exceptions like PermissionsError
181 $status = $ep->internalAttemptSave( $result, $bot );
182
183 if ( $expectedCode !== null ) {
184 // check edit code
185 $this->assertEquals( $expectedCode, $status->value,
186 "Expected result code mismatch. $message" );
187 }
188
189 $page = WikiPage::factory( $title );
190
191 if ( $expectedText !== null ) {
192 // check resulting page text
193 $content = $page->getContent();
194 $text = ContentHandler::getContentText( $content );
195
196 # EditPage rtrim() the user input, so we alter our expected text
197 # to reflect that.
198 $this->assertEditedTextEquals( $expectedText, $text,
199 "Expected article text mismatch. $message" );
200 }
201
202 return $page;
203 }
204
205 public static function provideCreatePages() {
206 return [
207 [ 'expected article being created',
208 'EditPageTest_testCreatePage',
209 null,
210 'Hello World!',
211 EditPage::AS_SUCCESS_NEW_ARTICLE,
212 'Hello World!'
213 ],
214 [ 'expected article not being created if empty',
215 'EditPageTest_testCreatePage',
216 null,
217 '',
218 EditPage::AS_BLANK_ARTICLE,
219 null
220 ],
221 [ 'expected MediaWiki: page being created',
222 'MediaWiki:January',
223 'UTSysop',
224 'Not January',
225 EditPage::AS_SUCCESS_NEW_ARTICLE,
226 'Not January'
227 ],
228 [ 'expected not-registered MediaWiki: page not being created if empty',
229 'MediaWiki:EditPageTest_testCreatePage',
230 'UTSysop',
231 '',
232 EditPage::AS_BLANK_ARTICLE,
233 null
234 ],
235 [ 'expected registered MediaWiki: page being created even if empty',
236 'MediaWiki:January',
237 'UTSysop',
238 '',
239 EditPage::AS_SUCCESS_NEW_ARTICLE,
240 ''
241 ],
242 [ 'expected registered MediaWiki: page whose default content is empty'
243 . ' not being created if empty',
244 'MediaWiki:Ipb-default-expiry',
245 'UTSysop',
246 '',
247 EditPage::AS_BLANK_ARTICLE,
248 ''
249 ],
250 [ 'expected MediaWiki: page not being created if text equals default message',
251 'MediaWiki:January',
252 'UTSysop',
253 'January',
254 EditPage::AS_BLANK_ARTICLE,
255 null
256 ],
257 [ 'expected empty article being created',
258 'EditPageTest_testCreatePage',
259 null,
260 '',
261 EditPage::AS_SUCCESS_NEW_ARTICLE,
262 '',
263 true
264 ],
265 ];
266 }
267
268 /**
269 * @dataProvider provideCreatePages
270 * @covers EditPage
271 */
272 public function testCreatePage(
273 $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
274 ) {
275 $checkId = null;
276
277 $this->setMwGlobals( 'wgHooks', [
278 'PageContentInsertComplete' => [ function (
279 WikiPage &$page, User &$user, Content $content,
280 $summary, $minor, $u1, $u2, &$flags, Revision $revision
281 ) {
282 // types/refs checked
283 } ],
284 'PageContentSaveComplete' => [ function (
285 WikiPage &$page, User &$user, Content $content,
286 $summary, $minor, $u1, $u2, &$flags, Revision $revision,
287 Status &$status, $baseRevId
288 ) use ( &$checkId ) {
289 $checkId = $status->value['revision']->getId();
290 // types/refs checked
291 } ],
292 ] );
293
294 $edit = [ 'wpTextbox1' => $editText ];
295 if ( $ignoreBlank ) {
296 $edit['wpIgnoreBlankArticle'] = 1;
297 }
298
299 $page = $this->assertEdit( $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
300
301 if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) {
302 $latest = $page->getLatest();
303 $page->doDeleteArticleReal( $pageTitle );
304
305 $this->assertGreaterThan( 0, $latest, "Page revision ID updated in object" );
306 $this->assertEquals( $latest, $checkId, "Revision in Status for hook" );
307 }
308 }
309
310 /**
311 * @dataProvider provideCreatePages
312 * @covers EditPage
313 */
314 public function testCreatePageTrx(
315 $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
316 ) {
317 $checkIds = [];
318 $this->setMwGlobals( 'wgHooks', [
319 'PageContentInsertComplete' => [ function (
320 WikiPage &$page, User &$user, Content $content,
321 $summary, $minor, $u1, $u2, &$flags, Revision $revision
322 ) {
323 // types/refs checked
324 } ],
325 'PageContentSaveComplete' => [ function (
326 WikiPage &$page, User &$user, Content $content,
327 $summary, $minor, $u1, $u2, &$flags, Revision $revision,
328 Status &$status, $baseRevId
329 ) use ( &$checkIds ) {
330 $checkIds[] = $status->value['revision']->getId();
331 // types/refs checked
332 } ],
333 ] );
334
335 wfGetDB( DB_MASTER )->begin( __METHOD__ );
336
337 $edit = [ 'wpTextbox1' => $editText ];
338 if ( $ignoreBlank ) {
339 $edit['wpIgnoreBlankArticle'] = 1;
340 }
341
342 $page = $this->assertEdit(
343 $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
344
345 $pageTitle2 = (string)$pageTitle . '/x';
346 $page2 = $this->assertEdit(
347 $pageTitle2, null, $user, $edit, $expectedCode, $expectedText, $desc );
348
349 wfGetDB( DB_MASTER )->commit( __METHOD__ );
350
351 $this->assertEquals( 0, DeferredUpdates::pendingUpdatesCount(), 'No deferred updates' );
352
353 if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) {
354 $latest = $page->getLatest();
355 $page->doDeleteArticleReal( $pageTitle );
356
357 $this->assertGreaterThan( 0, $latest, "Page #1 revision ID updated in object" );
358 $this->assertEquals( $latest, $checkIds[0], "Revision #1 in Status for hook" );
359
360 $latest2 = $page2->getLatest();
361 $page2->doDeleteArticleReal( $pageTitle2 );
362
363 $this->assertGreaterThan( 0, $latest2, "Page #2 revision ID updated in object" );
364 $this->assertEquals( $latest2, $checkIds[1], "Revision #2 in Status for hook" );
365 }
366 }
367
368 public function testUpdatePage() {
369 $checkIds = [];
370
371 $this->setMwGlobals( 'wgHooks', [
372 'PageContentInsertComplete' => [ function (
373 WikiPage &$page, User &$user, Content $content,
374 $summary, $minor, $u1, $u2, &$flags, Revision $revision
375 ) {
376 // types/refs checked
377 } ],
378 'PageContentSaveComplete' => [ function (
379 WikiPage &$page, User &$user, Content $content,
380 $summary, $minor, $u1, $u2, &$flags, Revision $revision,
381 Status &$status, $baseRevId
382 ) use ( &$checkIds ) {
383 $checkIds[] = $status->value['revision']->getId();
384 // types/refs checked
385 } ],
386 ] );
387
388 $text = "one";
389 $edit = [
390 'wpTextbox1' => $text,
391 'wpSummary' => 'first update',
392 ];
393
394 $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit,
395 EditPage::AS_SUCCESS_UPDATE, $text,
396 "expected successfull update with given text" );
397 $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
398
399 $this->forceRevisionDate( $page, '20120101000000' );
400
401 $text = "two";
402 $edit = [
403 'wpTextbox1' => $text,
404 'wpSummary' => 'second update',
405 ];
406
407 $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit,
408 EditPage::AS_SUCCESS_UPDATE, $text,
409 "expected successfull update with given text" );
410 $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
411 $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
412 }
413
414 public function testUpdatePageTrx() {
415 $text = "one";
416 $edit = [
417 'wpTextbox1' => $text,
418 'wpSummary' => 'first update',
419 ];
420
421 $page = $this->assertEdit( 'EditPageTest_testTrxUpdatePage', "zero", null, $edit,
422 EditPage::AS_SUCCESS_UPDATE, $text,
423 "expected successfull update with given text" );
424
425 $this->forceRevisionDate( $page, '20120101000000' );
426
427 $checkIds = [];
428 $this->setMwGlobals( 'wgHooks', [
429 'PageContentSaveComplete' => [ function (
430 WikiPage &$page, User &$user, Content $content,
431 $summary, $minor, $u1, $u2, &$flags, Revision $revision,
432 Status &$status, $baseRevId
433 ) use ( &$checkIds ) {
434 $checkIds[] = $status->value['revision']->getId();
435 // types/refs checked
436 } ],
437 ] );
438
439 wfGetDB( DB_MASTER )->begin( __METHOD__ );
440
441 $text = "two";
442 $edit = [
443 'wpTextbox1' => $text,
444 'wpSummary' => 'second update',
445 ];
446
447 $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
448 EditPage::AS_SUCCESS_UPDATE, $text,
449 "expected successfull update with given text" );
450
451 $text = "three";
452 $edit = [
453 'wpTextbox1' => $text,
454 'wpSummary' => 'third update',
455 ];
456
457 $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
458 EditPage::AS_SUCCESS_UPDATE, $text,
459 "expected successfull update with given text" );
460
461 wfGetDB( DB_MASTER )->commit( __METHOD__ );
462
463 $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
464 $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
465 $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
466 }
467
468 public static function provideSectionEdit() {
469 $text = 'Intro
470
471 == one ==
472 first section.
473
474 == two ==
475 second section.
476 ';
477
478 $sectionOne = '== one ==
479 hello
480 ';
481
482 $newSection = '== new section ==
483
484 hello
485 ';
486
487 $textWithNewSectionOne = preg_replace(
488 '/== one ==.*== two ==/ms',
489 "$sectionOne\n== two ==", $text
490 );
491
492 $textWithNewSectionAdded = "$text\n$newSection";
493
494 return [
495 [ # 0
496 $text,
497 '',
498 'hello',
499 'replace all',
500 'hello'
501 ],
502
503 [ # 1
504 $text,
505 '1',
506 $sectionOne,
507 'replace first section',
508 $textWithNewSectionOne,
509 ],
510
511 [ # 2
512 $text,
513 'new',
514 'hello',
515 'new section',
516 $textWithNewSectionAdded,
517 ],
518 ];
519 }
520
521 /**
522 * @dataProvider provideSectionEdit
523 * @covers EditPage
524 */
525 public function testSectionEdit( $base, $section, $text, $summary, $expected ) {
526 $edit = [
527 'wpTextbox1' => $text,
528 'wpSummary' => $summary,
529 'wpSection' => $section,
530 ];
531
532 $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit,
533 EditPage::AS_SUCCESS_UPDATE, $expected,
534 "expected successfull update of section" );
535 }
536
537 public static function provideAutoMerge() {
538 $tests = [];
539
540 $tests[] = [ # 0: plain conflict
541 "Elmo", # base edit user
542 "one\n\ntwo\n\nthree\n",
543 [ # adam's edit
544 'wpStarttime' => 1,
545 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
546 ],
547 [ # berta's edit
548 'wpStarttime' => 2,
549 'wpTextbox1' => "(one)\n\ntwo\n\nthree\n",
550 ],
551 EditPage::AS_CONFLICT_DETECTED, # expected code
552 "ONE\n\ntwo\n\nthree\n", # expected text
553 'expected edit conflict', # message
554 ];
555
556 $tests[] = [ # 1: successful merge
557 "Elmo", # base edit user
558 "one\n\ntwo\n\nthree\n",
559 [ # adam's edit
560 'wpStarttime' => 1,
561 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
562 ],
563 [ # berta's edit
564 'wpStarttime' => 2,
565 'wpTextbox1' => "one\n\ntwo\n\nTHREE\n",
566 ],
567 EditPage::AS_SUCCESS_UPDATE, # expected code
568 "ONE\n\ntwo\n\nTHREE\n", # expected text
569 'expected automatic merge', # message
570 ];
571
572 $text = "Intro\n\n";
573 $text .= "== first section ==\n\n";
574 $text .= "one\n\ntwo\n\nthree\n\n";
575 $text .= "== second section ==\n\n";
576 $text .= "four\n\nfive\n\nsix\n\n";
577
578 // extract the first section.
579 $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text );
580
581 // generate expected text after merge
582 $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) );
583
584 $tests[] = [ # 2: merge in section
585 "Elmo", # base edit user
586 $text,
587 [ # adam's edit
588 'wpStarttime' => 1,
589 'wpTextbox1' => str_replace( 'one', 'ONE', $section ),
590 'wpSection' => '1'
591 ],
592 [ # berta's edit
593 'wpStarttime' => 2,
594 'wpTextbox1' => str_replace( 'three', 'THREE', $section ),
595 'wpSection' => '1'
596 ],
597 EditPage::AS_SUCCESS_UPDATE, # expected code
598 $expected, # expected text
599 'expected automatic section merge', # message
600 ];
601
602 // see whether it makes a difference who did the base edit
603 $testsWithAdam = array_map( function ( $test ) {
604 $test[0] = 'Adam'; // change base edit user
605 return $test;
606 }, $tests );
607
608 $testsWithBerta = array_map( function ( $test ) {
609 $test[0] = 'Berta'; // change base edit user
610 return $test;
611 }, $tests );
612
613 return array_merge( $tests, $testsWithAdam, $testsWithBerta );
614 }
615
616 /**
617 * @dataProvider provideAutoMerge
618 * @covers EditPage
619 */
620 public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit,
621 $expectedCode, $expectedText, $message = null
622 ) {
623 $this->markTestSkippedIfNoDiff3();
624
625 // create page
626 $ns = $this->getDefaultWikitextNS();
627 $title = Title::newFromText( 'EditPageTest_testAutoMerge', $ns );
628 $page = WikiPage::factory( $title );
629
630 if ( $page->exists() ) {
631 $page->doDeleteArticle( "clean slate for testing" );
632 }
633
634 $baseEdit = [
635 'wpTextbox1' => $text,
636 ];
637
638 $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null,
639 $baseUser, $baseEdit, null, null, __METHOD__ );
640
641 $this->forceRevisionDate( $page, '20120101000000' );
642
643 $edittime = $page->getTimestamp();
644
645 // start timestamps for conflict detection
646 if ( !isset( $adamsEdit['wpStarttime'] ) ) {
647 $adamsEdit['wpStarttime'] = 1;
648 }
649
650 if ( !isset( $bertasEdit['wpStarttime'] ) ) {
651 $bertasEdit['wpStarttime'] = 2;
652 }
653
654 $starttime = wfTimestampNow();
655 $adamsTime = wfTimestamp(
656 TS_MW,
657 (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$adamsEdit['wpStarttime']
658 );
659 $bertasTime = wfTimestamp(
660 TS_MW,
661 (int)wfTimestamp( TS_UNIX, $starttime ) + (int)$bertasEdit['wpStarttime']
662 );
663
664 $adamsEdit['wpStarttime'] = $adamsTime;
665 $bertasEdit['wpStarttime'] = $bertasTime;
666
667 $adamsEdit['wpSummary'] = 'Adam\'s edit';
668 $bertasEdit['wpSummary'] = 'Bertas\'s edit';
669
670 $adamsEdit['wpEdittime'] = $edittime;
671 $bertasEdit['wpEdittime'] = $edittime;
672
673 // first edit
674 $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit,
675 EditPage::AS_SUCCESS_UPDATE, null, "expected successfull update" );
676
677 // second edit
678 $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
679 $expectedCode, $expectedText, $message );
680 }
681
682 /**
683 * @depends testAutoMerge
684 */
685 public function testCheckDirectEditingDisallowed_forNonTextContent() {
686 $title = Title::newFromText( 'Dummy:NonTextPageForEditPage' );
687 $page = WikiPage::factory( $title );
688
689 $article = new Article( $title );
690 $article->getContext()->setTitle( $title );
691 $ep = new EditPage( $article );
692 $ep->setContextTitle( $title );
693
694 $user = $GLOBALS['wgUser'];
695
696 $edit = [
697 'wpTextbox1' => serialize( 'non-text content' ),
698 'wpEditToken' => $user->getEditToken(),
699 'wpEdittime' => '',
700 'wpStarttime' => wfTimestampNow()
701 ];
702
703 $req = new FauxRequest( $edit, true );
704 $ep->importFormData( $req );
705
706 $this->setExpectedException(
707 'MWException',
708 'This content model is not supported: testing'
709 );
710
711 $ep->internalAttemptSave( $result, false );
712 }
713
714 }