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