3 use MediaWiki\MediaWikiServices
;
9 * ^--- tell jenkins this test needs the database
12 * ^--- tell phpunit that these test cases may take longer than 2 seconds.
14 class EditPageTest
extends MediaWikiLangTestCase
{
16 protected function setUp() {
19 $contLang = MediaWikiServices
::getInstance()->getContentLanguage();
20 $this->setContentLang( $contLang );
22 $this->setMwGlobals( [
23 'wgExtraNamespaces' => [
25 12313 => 'Dummy_talk',
27 'wgNamespaceContentModels' => [ 12312 => 'testing' ],
29 $this->mergeMwGlobalArrayValue(
31 [ 'testing' => 'DummyContentHandlerForTesting' ]
36 * @dataProvider provideExtractSectionTitle
37 * @covers EditPage::extractSectionTitle
39 public function testExtractSectionTitle( $section, $title ) {
40 $extracted = EditPage
::extractSectionTitle( $section );
41 $this->assertEquals( $title, $extracted );
44 public static function provideExtractSectionTitle() {
47 "== Test ==\n\nJust a test section.",
51 "An initial section, no header.",
55 "An initial section with a fake heder (T34617)\n\n== Test == ??\nwtf",
59 "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo",
63 "== Section== \t\r\n followed by whitespace (T37051)",
69 protected function forceRevisionDate( WikiPage
$page, $timestamp ) {
70 $dbw = wfGetDB( DB_MASTER
);
72 $dbw->update( 'revision',
73 [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
74 [ 'rev_id' => $page->getLatest() ] );
80 * User input text is passed to rtrim() by edit page. This is a simple
81 * wrapper around assertEquals() which calls rrtrim() to normalize the
82 * expected and actual texts.
83 * @param string $expected
84 * @param string $actual
87 protected function assertEditedTextEquals( $expected, $actual, $msg = '' ) {
88 $this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg );
92 * Performs an edit and checks the result.
94 * @param string|Title $title The title of the page to edit
95 * @param string|null $baseText Some text to create the page with before attempting the edit.
96 * @param User|string|null $user The user to perform the edit as.
97 * @param array $edit An array of request parameters used to define the edit to perform.
98 * Some well known fields are:
99 * * wpTextbox1: the text to submit
100 * * wpSummary: the edit summary
101 * * wpEditToken: the edit token (will be inserted if not provided)
102 * * wpEdittime: timestamp of the edit's base revision (will be inserted
104 * * wpStarttime: timestamp when the edit started (will be inserted if not provided)
105 * * wpSectionTitle: the section to edit
106 * * wpMinorEdit: mark as minor edit
107 * * wpWatchthis: whether to watch the page
108 * @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants).
109 * Set to null to skip the check.
110 * @param string|null $expectedText The text expected to be on the page after the edit.
111 * Set to null to skip the check.
112 * @param string|null $message An optional message to show along with any error message.
114 * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
116 protected function assertEdit( $title, $baseText, $user = null, array $edit,
117 $expectedCode = null, $expectedText = null, $message = null
119 if ( is_string( $title ) ) {
120 $ns = $this->getDefaultWikitextNS();
121 $title = Title
::newFromText( $title, $ns );
123 $this->assertNotNull( $title );
125 if ( is_string( $user ) ) {
126 $user = User
::newFromName( $user );
128 if ( $user->getId() === 0 ) {
129 $user->addToDatabase();
133 $page = WikiPage
::factory( $title );
135 if ( $baseText !== null ) {
136 $content = ContentHandler
::makeContent( $baseText, $title );
137 $page->doEditContent( $content, "base text for test" );
138 $this->forceRevisionDate( $page, '20120101000000' );
142 $currentText = ContentHandler
::getContentText( $page->getContent() );
144 # EditPage rtrim() the user input, so we alter our expected text
146 $this->assertEditedTextEquals( $baseText, $currentText );
149 if ( $user == null ) {
150 $user = $GLOBALS['wgUser'];
152 $this->setMwGlobals( 'wgUser', $user );
155 if ( !isset( $edit['wpEditToken'] ) ) {
156 $edit['wpEditToken'] = $user->getEditToken();
159 if ( !isset( $edit['wpEdittime'] ) ) {
160 $edit['wpEdittime'] = $page->exists() ?
$page->getTimestamp() : '';
163 if ( !isset( $edit['wpStarttime'] ) ) {
164 $edit['wpStarttime'] = wfTimestampNow();
167 if ( !isset( $edit['wpUnicodeCheck'] ) ) {
168 $edit['wpUnicodeCheck'] = EditPage
::UNICODE_CHECK
;
171 $req = new FauxRequest( $edit, true ); // session ??
173 $article = new Article( $title );
174 $article->getContext()->setTitle( $title );
175 $ep = new EditPage( $article );
176 $ep->setContextTitle( $title );
177 $ep->importFormData( $req );
179 $bot = isset( $edit['bot'] ) ?
(bool)$edit['bot'] : false;
181 // this is where the edit happens!
182 // Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut
183 // and throws exceptions like PermissionsError
184 $status = $ep->internalAttemptSave( $result, $bot );
186 if ( $expectedCode !== null ) {
188 $this->assertEquals( $expectedCode, $status->value
,
189 "Expected result code mismatch. $message" );
192 $page = WikiPage
::factory( $title );
194 if ( $expectedText !== null ) {
195 // check resulting page text
196 $content = $page->getContent();
197 $text = ContentHandler
::getContentText( $content );
199 # EditPage rtrim() the user input, so we alter our expected text
201 $this->assertEditedTextEquals( $expectedText, $text,
202 "Expected article text mismatch. $message" );
208 public static function provideCreatePages() {
210 [ 'expected article being created',
211 'EditPageTest_testCreatePage',
214 EditPage
::AS_SUCCESS_NEW_ARTICLE
,
217 [ 'expected article not being created if empty',
218 'EditPageTest_testCreatePage',
221 EditPage
::AS_BLANK_ARTICLE
,
224 [ 'expected MediaWiki: page being created',
228 EditPage
::AS_SUCCESS_NEW_ARTICLE
,
231 [ 'expected not-registered MediaWiki: page not being created if empty',
232 'MediaWiki:EditPageTest_testCreatePage',
235 EditPage
::AS_BLANK_ARTICLE
,
238 [ 'expected registered MediaWiki: page being created even if empty',
242 EditPage
::AS_SUCCESS_NEW_ARTICLE
,
245 [ 'expected registered MediaWiki: page whose default content is empty'
246 . ' not being created if empty',
247 'MediaWiki:Ipb-default-expiry',
250 EditPage
::AS_BLANK_ARTICLE
,
253 [ 'expected MediaWiki: page not being created if text equals default message',
257 EditPage
::AS_BLANK_ARTICLE
,
260 [ 'expected empty article being created',
261 'EditPageTest_testCreatePage',
264 EditPage
::AS_SUCCESS_NEW_ARTICLE
,
272 * @dataProvider provideCreatePages
275 public function testCreatePage(
276 $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
280 $this->setMwGlobals( 'wgHooks', [
281 'PageContentInsertComplete' => [ function (
282 WikiPage
&$page, User
&$user, Content
$content,
283 $summary, $minor, $u1, $u2, &$flags, Revision
$revision
285 // types/refs checked
287 'PageContentSaveComplete' => [ function (
288 WikiPage
&$page, User
&$user, Content
$content,
289 $summary, $minor, $u1, $u2, &$flags, Revision
$revision,
290 Status
&$status, $baseRevId
291 ) use ( &$checkId ) {
292 $checkId = $status->value
['revision']->getId();
293 // types/refs checked
297 $edit = [ 'wpTextbox1' => $editText ];
298 if ( $ignoreBlank ) {
299 $edit['wpIgnoreBlankArticle'] = 1;
302 $page = $this->assertEdit( $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
304 if ( $expectedCode != EditPage
::AS_BLANK_ARTICLE
) {
305 $latest = $page->getLatest();
306 $page->doDeleteArticleReal( $pageTitle );
308 $this->assertGreaterThan( 0, $latest, "Page revision ID updated in object" );
309 $this->assertEquals( $latest, $checkId, "Revision in Status for hook" );
314 * @dataProvider provideCreatePages
317 public function testCreatePageTrx(
318 $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
321 $this->setMwGlobals( 'wgHooks', [
322 'PageContentInsertComplete' => [ function (
323 WikiPage
&$page, User
&$user, Content
$content,
324 $summary, $minor, $u1, $u2, &$flags, Revision
$revision
326 // types/refs checked
328 'PageContentSaveComplete' => [ function (
329 WikiPage
&$page, User
&$user, Content
$content,
330 $summary, $minor, $u1, $u2, &$flags, Revision
$revision,
331 Status
&$status, $baseRevId
332 ) use ( &$checkIds ) {
333 $checkIds[] = $status->value
['revision']->getId();
334 // types/refs checked
338 wfGetDB( DB_MASTER
)->begin( __METHOD__
);
340 $edit = [ 'wpTextbox1' => $editText ];
341 if ( $ignoreBlank ) {
342 $edit['wpIgnoreBlankArticle'] = 1;
345 $page = $this->assertEdit(
346 $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
348 $pageTitle2 = (string)$pageTitle . '/x';
349 $page2 = $this->assertEdit(
350 $pageTitle2, null, $user, $edit, $expectedCode, $expectedText, $desc );
352 wfGetDB( DB_MASTER
)->commit( __METHOD__
);
354 $this->assertEquals( 0, DeferredUpdates
::pendingUpdatesCount(), 'No deferred updates' );
356 if ( $expectedCode != EditPage
::AS_BLANK_ARTICLE
) {
357 $latest = $page->getLatest();
358 $page->doDeleteArticleReal( $pageTitle );
360 $this->assertGreaterThan( 0, $latest, "Page #1 revision ID updated in object" );
361 $this->assertEquals( $latest, $checkIds[0], "Revision #1 in Status for hook" );
363 $latest2 = $page2->getLatest();
364 $page2->doDeleteArticleReal( $pageTitle2 );
366 $this->assertGreaterThan( 0, $latest2, "Page #2 revision ID updated in object" );
367 $this->assertEquals( $latest2, $checkIds[1], "Revision #2 in Status for hook" );
371 public function testUpdatePage() {
374 $this->setMwGlobals( 'wgHooks', [
375 'PageContentInsertComplete' => [ function (
376 WikiPage
&$page, User
&$user, Content
$content,
377 $summary, $minor, $u1, $u2, &$flags, Revision
$revision
379 // types/refs checked
381 'PageContentSaveComplete' => [ function (
382 WikiPage
&$page, User
&$user, Content
$content,
383 $summary, $minor, $u1, $u2, &$flags, Revision
$revision,
384 Status
&$status, $baseRevId
385 ) use ( &$checkIds ) {
386 $checkIds[] = $status->value
['revision']->getId();
387 // types/refs checked
393 'wpTextbox1' => $text,
394 'wpSummary' => 'first update',
397 $page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit,
398 EditPage
::AS_SUCCESS_UPDATE
, $text,
399 "expected successful update with given text" );
400 $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
402 $this->forceRevisionDate( $page, '20120101000000' );
406 'wpTextbox1' => $text,
407 'wpSummary' => 'second update',
410 $this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit,
411 EditPage
::AS_SUCCESS_UPDATE
, $text,
412 "expected successful update with given text" );
413 $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
414 $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
417 public function testUpdatePageTrx() {
420 'wpTextbox1' => $text,
421 'wpSummary' => 'first update',
424 $page = $this->assertEdit( 'EditPageTest_testTrxUpdatePage', "zero", null, $edit,
425 EditPage
::AS_SUCCESS_UPDATE
, $text,
426 "expected successful update with given text" );
428 $this->forceRevisionDate( $page, '20120101000000' );
431 $this->setMwGlobals( 'wgHooks', [
432 'PageContentSaveComplete' => [ function (
433 WikiPage
&$page, User
&$user, Content
$content,
434 $summary, $minor, $u1, $u2, &$flags, Revision
$revision,
435 Status
&$status, $baseRevId
436 ) use ( &$checkIds ) {
437 $checkIds[] = $status->value
['revision']->getId();
438 // types/refs checked
442 wfGetDB( DB_MASTER
)->begin( __METHOD__
);
446 'wpTextbox1' => $text,
447 'wpSummary' => 'second update',
450 $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
451 EditPage
::AS_SUCCESS_UPDATE
, $text,
452 "expected successful update with given text" );
456 'wpTextbox1' => $text,
457 'wpSummary' => 'third update',
460 $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
461 EditPage
::AS_SUCCESS_UPDATE
, $text,
462 "expected successful update with given text" );
464 wfGetDB( DB_MASTER
)->commit( __METHOD__
);
466 $this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
467 $this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
468 $this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
471 public static function provideSectionEdit() {
481 $sectionOne = '== one ==
485 $newSection = '== new section ==
490 $textWithNewSectionOne = preg_replace(
491 '/== one ==.*== two ==/ms',
492 "$sectionOne\n== two ==", $text
495 $textWithNewSectionAdded = "$text\n$newSection";
510 'replace first section',
511 $textWithNewSectionOne,
519 $textWithNewSectionAdded,
525 * @dataProvider provideSectionEdit
528 public function testSectionEdit( $base, $section, $text, $summary, $expected ) {
530 'wpTextbox1' => $text,
531 'wpSummary' => $summary,
532 'wpSection' => $section,
535 $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit,
536 EditPage
::AS_SUCCESS_UPDATE
, $expected,
537 "expected successful update of section" );
540 public static function provideAutoMerge() {
543 $tests[] = [ # 0: plain conflict
544 "Elmo", # base edit user
545 "one\n\ntwo\n\nthree\n",
548 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
552 'wpTextbox1' => "(one)\n\ntwo\n\nthree\n",
554 EditPage
::AS_CONFLICT_DETECTED
, # expected code
555 "ONE\n\ntwo\n\nthree\n", # expected text
556 'expected edit conflict', # message
559 $tests[] = [ # 1: successful merge
560 "Elmo", # base edit user
561 "one\n\ntwo\n\nthree\n",
564 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
568 'wpTextbox1' => "one\n\ntwo\n\nTHREE\n",
570 EditPage
::AS_SUCCESS_UPDATE
, # expected code
571 "ONE\n\ntwo\n\nTHREE\n", # expected text
572 'expected automatic merge', # message
576 $text .= "== first section ==\n\n";
577 $text .= "one\n\ntwo\n\nthree\n\n";
578 $text .= "== second section ==\n\n";
579 $text .= "four\n\nfive\n\nsix\n\n";
581 // extract the first section.
582 $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text );
584 // generate expected text after merge
585 $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) );
587 $tests[] = [ # 2: merge in section
588 "Elmo", # base edit user
592 'wpTextbox1' => str_replace( 'one', 'ONE', $section ),
597 'wpTextbox1' => str_replace( 'three', 'THREE', $section ),
600 EditPage
::AS_SUCCESS_UPDATE
, # expected code
601 $expected, # expected text
602 'expected automatic section merge', # message
605 // see whether it makes a difference who did the base edit
606 $testsWithAdam = array_map( function ( $test ) {
607 $test[0] = 'Adam'; // change base edit user
611 $testsWithBerta = array_map( function ( $test ) {
612 $test[0] = 'Berta'; // change base edit user
616 return array_merge( $tests, $testsWithAdam, $testsWithBerta );
620 * @dataProvider provideAutoMerge
623 public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit,
624 $expectedCode, $expectedText, $message = null
626 $this->markTestSkippedIfNoDiff3();
629 $ns = $this->getDefaultWikitextNS();
630 $title = Title
::newFromText( 'EditPageTest_testAutoMerge', $ns );
631 $page = WikiPage
::factory( $title );
633 if ( $page->exists() ) {
634 $page->doDeleteArticle( "clean slate for testing" );
638 'wpTextbox1' => $text,
641 $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null,
642 $baseUser, $baseEdit, null, null, __METHOD__
);
644 $this->forceRevisionDate( $page, '20120101000000' );
646 $edittime = $page->getTimestamp();
648 // start timestamps for conflict detection
649 if ( !isset( $adamsEdit['wpStarttime'] ) ) {
650 $adamsEdit['wpStarttime'] = 1;
653 if ( !isset( $bertasEdit['wpStarttime'] ) ) {
654 $bertasEdit['wpStarttime'] = 2;
657 $starttime = wfTimestampNow();
658 $adamsTime = wfTimestamp(
660 (int)wfTimestamp( TS_UNIX
, $starttime ) +
(int)$adamsEdit['wpStarttime']
662 $bertasTime = wfTimestamp(
664 (int)wfTimestamp( TS_UNIX
, $starttime ) +
(int)$bertasEdit['wpStarttime']
667 $adamsEdit['wpStarttime'] = $adamsTime;
668 $bertasEdit['wpStarttime'] = $bertasTime;
670 $adamsEdit['wpSummary'] = 'Adam\'s edit';
671 $bertasEdit['wpSummary'] = 'Bertas\'s edit';
673 $adamsEdit['wpEdittime'] = $edittime;
674 $bertasEdit['wpEdittime'] = $edittime;
677 $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit,
678 EditPage
::AS_SUCCESS_UPDATE
, null, "expected successful update" );
681 $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
682 $expectedCode, $expectedText, $message );
686 * @depends testAutoMerge
688 public function testCheckDirectEditingDisallowed_forNonTextContent() {
689 $title = Title
::newFromText( 'Dummy:NonTextPageForEditPage' );
690 $page = WikiPage
::factory( $title );
692 $article = new Article( $title );
693 $article->getContext()->setTitle( $title );
694 $ep = new EditPage( $article );
695 $ep->setContextTitle( $title );
697 $user = $GLOBALS['wgUser'];
700 'wpTextbox1' => serialize( 'non-text content' ),
701 'wpEditToken' => $user->getEditToken(),
703 'wpStarttime' => wfTimestampNow(),
704 'wpUnicodeCheck' => EditPage
::UNICODE_CHECK
,
707 $req = new FauxRequest( $edit, true );
708 $ep->importFormData( $req );
710 $this->setExpectedException(
712 'This content model is not supported: testing'
715 $ep->internalAttemptSave( $result, false );