7 * ^--- tell jenkins this test needs the database
10 * ^--- tell phpunit that these test cases may take longer than 2 seconds.
12 class EditPageTest
extends MediaWikiLangTestCase
{
14 protected function setUp() {
17 $this->setMwGlobals( [
18 'wgExtraNamespaces' => [
20 12313 => 'Dummy_talk',
22 'wgNamespaceContentModels' => [ 12312 => 'testing' ],
24 $this->mergeMwGlobalArrayValue(
26 [ 'testing' => 'DummyContentHandlerForTesting' ]
31 * @dataProvider provideExtractSectionTitle
32 * @covers EditPage::extractSectionTitle
34 public function testExtractSectionTitle( $section, $title ) {
35 $extracted = EditPage
::extractSectionTitle( $section );
36 $this->assertEquals( $title, $extracted );
39 public static function provideExtractSectionTitle() {
42 "== Test ==\n\nJust a test section.",
46 "An initial section, no header.",
50 "An initial section with a fake heder (T34617)\n\n== Test == ??\nwtf",
54 "== Section ==\nfollowed by a fake == Non-section == ??\nnoooo",
58 "== Section== \t\r\n followed by whitespace (T37051)",
64 protected function forceRevisionDate( WikiPage
$page, $timestamp ) {
65 $dbw = wfGetDB( DB_MASTER
);
67 $dbw->update( 'revision',
68 [ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
69 [ 'rev_id' => $page->getLatest() ] );
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
82 protected function assertEditedTextEquals( $expected, $actual, $msg = '' ) {
83 $this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg );
87 * Performs an edit and checks the result.
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
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.
109 * @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
111 protected function assertEdit( $title, $baseText, $user = null, array $edit,
112 $expectedCode = null, $expectedText = null, $message = null
114 if ( is_string( $title ) ) {
115 $ns = $this->getDefaultWikitextNS();
116 $title = Title
::newFromText( $title, $ns );
118 $this->assertNotNull( $title );
120 if ( is_string( $user ) ) {
121 $user = User
::newFromName( $user );
123 if ( $user->getId() === 0 ) {
124 $user->addToDatabase();
128 $page = WikiPage
::factory( $title );
130 if ( $baseText !== null ) {
131 $content = ContentHandler
::makeContent( $baseText, $title );
132 $page->doEditContent( $content, "base text for test" );
133 $this->forceRevisionDate( $page, '20120101000000' );
137 $currentText = ContentHandler
::getContentText( $page->getContent() );
139 # EditPage rtrim() the user input, so we alter our expected text
141 $this->assertEditedTextEquals( $baseText, $currentText );
144 if ( $user == null ) {
145 $user = $GLOBALS['wgUser'];
147 $this->setMwGlobals( 'wgUser', $user );
150 if ( !isset( $edit['wpEditToken'] ) ) {
151 $edit['wpEditToken'] = $user->getEditToken();
154 if ( !isset( $edit['wpEdittime'] ) ) {
155 $edit['wpEdittime'] = $page->exists() ?
$page->getTimestamp() : '';
158 if ( !isset( $edit['wpStarttime'] ) ) {
159 $edit['wpStarttime'] = wfTimestampNow();
162 if ( !isset( $edit['wpUnicodeCheck'] ) ) {
163 $edit['wpUnicodeCheck'] = EditPage
::UNICODE_CHECK
;
166 $req = new FauxRequest( $edit, true ); // session ??
168 $article = new Article( $title );
169 $article->getContext()->setTitle( $title );
170 $ep = new EditPage( $article );
171 $ep->setContextTitle( $title );
172 $ep->importFormData( $req );
174 $bot = isset( $edit['bot'] ) ?
(bool)$edit['bot'] : false;
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 );
181 if ( $expectedCode !== null ) {
183 $this->assertEquals( $expectedCode, $status->value
,
184 "Expected result code mismatch. $message" );
187 $page = WikiPage
::factory( $title );
189 if ( $expectedText !== null ) {
190 // check resulting page text
191 $content = $page->getContent();
192 $text = ContentHandler
::getContentText( $content );
194 # EditPage rtrim() the user input, so we alter our expected text
196 $this->assertEditedTextEquals( $expectedText, $text,
197 "Expected article text mismatch. $message" );
203 public static function provideCreatePages() {
205 [ 'expected article being created',
206 'EditPageTest_testCreatePage',
209 EditPage
::AS_SUCCESS_NEW_ARTICLE
,
212 [ 'expected article not being created if empty',
213 'EditPageTest_testCreatePage',
216 EditPage
::AS_BLANK_ARTICLE
,
219 [ 'expected MediaWiki: page being created',
223 EditPage
::AS_SUCCESS_NEW_ARTICLE
,
226 [ 'expected not-registered MediaWiki: page not being created if empty',
227 'MediaWiki:EditPageTest_testCreatePage',
230 EditPage
::AS_BLANK_ARTICLE
,
233 [ 'expected registered MediaWiki: page being created even if empty',
237 EditPage
::AS_SUCCESS_NEW_ARTICLE
,
240 [ 'expected registered MediaWiki: page whose default content is empty'
241 . ' not being created if empty',
242 'MediaWiki:Ipb-default-expiry',
245 EditPage
::AS_BLANK_ARTICLE
,
248 [ 'expected MediaWiki: page not being created if text equals default message',
252 EditPage
::AS_BLANK_ARTICLE
,
255 [ 'expected empty article being created',
256 'EditPageTest_testCreatePage',
259 EditPage
::AS_SUCCESS_NEW_ARTICLE
,
267 * @dataProvider provideCreatePages
270 public function testCreatePage(
271 $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
275 $this->setMwGlobals( 'wgHooks', [
276 'PageContentInsertComplete' => [ function (
277 WikiPage
&$page, User
&$user, Content
$content,
278 $summary, $minor, $u1, $u2, &$flags, Revision
$revision
280 // types/refs checked
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
292 $edit = [ 'wpTextbox1' => $editText ];
293 if ( $ignoreBlank ) {
294 $edit['wpIgnoreBlankArticle'] = 1;
297 $page = $this->assertEdit( $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
299 if ( $expectedCode != EditPage
::AS_BLANK_ARTICLE
) {
300 $latest = $page->getLatest();
301 $page->doDeleteArticleReal( $pageTitle );
303 $this->assertGreaterThan( 0, $latest, "Page revision ID updated in object" );
304 $this->assertEquals( $latest, $checkId, "Revision in Status for hook" );
309 * @dataProvider provideCreatePages
312 public function testCreatePageTrx(
313 $desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
316 $this->setMwGlobals( 'wgHooks', [
317 'PageContentInsertComplete' => [ function (
318 WikiPage
&$page, User
&$user, Content
$content,
319 $summary, $minor, $u1, $u2, &$flags, Revision
$revision
321 // types/refs checked
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
333 wfGetDB( DB_MASTER
)->begin( __METHOD__
);
335 $edit = [ 'wpTextbox1' => $editText ];
336 if ( $ignoreBlank ) {
337 $edit['wpIgnoreBlankArticle'] = 1;
340 $page = $this->assertEdit(
341 $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
343 $pageTitle2 = (string)$pageTitle . '/x';
344 $page2 = $this->assertEdit(
345 $pageTitle2, null, $user, $edit, $expectedCode, $expectedText, $desc );
347 wfGetDB( DB_MASTER
)->commit( __METHOD__
);
349 $this->assertEquals( 0, DeferredUpdates
::pendingUpdatesCount(), 'No deferred updates' );
351 if ( $expectedCode != EditPage
::AS_BLANK_ARTICLE
) {
352 $latest = $page->getLatest();
353 $page->doDeleteArticleReal( $pageTitle );
355 $this->assertGreaterThan( 0, $latest, "Page #1 revision ID updated in object" );
356 $this->assertEquals( $latest, $checkIds[0], "Revision #1 in Status for hook" );
358 $latest2 = $page2->getLatest();
359 $page2->doDeleteArticleReal( $pageTitle2 );
361 $this->assertGreaterThan( 0, $latest2, "Page #2 revision ID updated in object" );
362 $this->assertEquals( $latest2, $checkIds[1], "Revision #2 in Status for hook" );
366 public function testUpdatePage() {
369 $this->setMwGlobals( 'wgHooks', [
370 'PageContentInsertComplete' => [ function (
371 WikiPage
&$page, User
&$user, Content
$content,
372 $summary, $minor, $u1, $u2, &$flags, Revision
$revision
374 // types/refs checked
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
388 'wpTextbox1' => $text,
389 'wpSummary' => 'first update',
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" );
397 $this->forceRevisionDate( $page, '20120101000000' );
401 'wpTextbox1' => $text,
402 'wpSummary' => 'second update',
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" );
412 public function testUpdatePageTrx() {
415 'wpTextbox1' => $text,
416 'wpSummary' => 'first update',
419 $page = $this->assertEdit( 'EditPageTest_testTrxUpdatePage', "zero", null, $edit,
420 EditPage
::AS_SUCCESS_UPDATE
, $text,
421 "expected successfull update with given text" );
423 $this->forceRevisionDate( $page, '20120101000000' );
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
437 wfGetDB( DB_MASTER
)->begin( __METHOD__
);
441 'wpTextbox1' => $text,
442 'wpSummary' => 'second update',
445 $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
446 EditPage
::AS_SUCCESS_UPDATE
, $text,
447 "expected successfull update with given text" );
451 'wpTextbox1' => $text,
452 'wpSummary' => 'third update',
455 $this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
456 EditPage
::AS_SUCCESS_UPDATE
, $text,
457 "expected successfull update with given text" );
459 wfGetDB( DB_MASTER
)->commit( __METHOD__
);
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" );
466 public static function provideSectionEdit() {
476 $sectionOne = '== one ==
480 $newSection = '== new section ==
485 $textWithNewSectionOne = preg_replace(
486 '/== one ==.*== two ==/ms',
487 "$sectionOne\n== two ==", $text
490 $textWithNewSectionAdded = "$text\n$newSection";
505 'replace first section',
506 $textWithNewSectionOne,
514 $textWithNewSectionAdded,
520 * @dataProvider provideSectionEdit
523 public function testSectionEdit( $base, $section, $text, $summary, $expected ) {
525 'wpTextbox1' => $text,
526 'wpSummary' => $summary,
527 'wpSection' => $section,
530 $this->assertEdit( 'EditPageTest_testSectionEdit', $base, null, $edit,
531 EditPage
::AS_SUCCESS_UPDATE
, $expected,
532 "expected successfull update of section" );
535 public static function provideAutoMerge() {
538 $tests[] = [ # 0: plain conflict
539 "Elmo", # base edit user
540 "one\n\ntwo\n\nthree\n",
543 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
547 'wpTextbox1' => "(one)\n\ntwo\n\nthree\n",
549 EditPage
::AS_CONFLICT_DETECTED
, # expected code
550 "ONE\n\ntwo\n\nthree\n", # expected text
551 'expected edit conflict', # message
554 $tests[] = [ # 1: successful merge
555 "Elmo", # base edit user
556 "one\n\ntwo\n\nthree\n",
559 'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
563 'wpTextbox1' => "one\n\ntwo\n\nTHREE\n",
565 EditPage
::AS_SUCCESS_UPDATE
, # expected code
566 "ONE\n\ntwo\n\nTHREE\n", # expected text
567 'expected automatic merge', # message
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";
576 // extract the first section.
577 $section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text );
579 // generate expected text after merge
580 $expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) );
582 $tests[] = [ # 2: merge in section
583 "Elmo", # base edit user
587 'wpTextbox1' => str_replace( 'one', 'ONE', $section ),
592 'wpTextbox1' => str_replace( 'three', 'THREE', $section ),
595 EditPage
::AS_SUCCESS_UPDATE
, # expected code
596 $expected, # expected text
597 'expected automatic section merge', # message
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
606 $testsWithBerta = array_map( function ( $test ) {
607 $test[0] = 'Berta'; // change base edit user
611 return array_merge( $tests, $testsWithAdam, $testsWithBerta );
615 * @dataProvider provideAutoMerge
618 public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit,
619 $expectedCode, $expectedText, $message = null
621 $this->markTestSkippedIfNoDiff3();
624 $ns = $this->getDefaultWikitextNS();
625 $title = Title
::newFromText( 'EditPageTest_testAutoMerge', $ns );
626 $page = WikiPage
::factory( $title );
628 if ( $page->exists() ) {
629 $page->doDeleteArticle( "clean slate for testing" );
633 'wpTextbox1' => $text,
636 $page = $this->assertEdit( 'EditPageTest_testAutoMerge', null,
637 $baseUser, $baseEdit, null, null, __METHOD__
);
639 $this->forceRevisionDate( $page, '20120101000000' );
641 $edittime = $page->getTimestamp();
643 // start timestamps for conflict detection
644 if ( !isset( $adamsEdit['wpStarttime'] ) ) {
645 $adamsEdit['wpStarttime'] = 1;
648 if ( !isset( $bertasEdit['wpStarttime'] ) ) {
649 $bertasEdit['wpStarttime'] = 2;
652 $starttime = wfTimestampNow();
653 $adamsTime = wfTimestamp(
655 (int)wfTimestamp( TS_UNIX
, $starttime ) +
(int)$adamsEdit['wpStarttime']
657 $bertasTime = wfTimestamp(
659 (int)wfTimestamp( TS_UNIX
, $starttime ) +
(int)$bertasEdit['wpStarttime']
662 $adamsEdit['wpStarttime'] = $adamsTime;
663 $bertasEdit['wpStarttime'] = $bertasTime;
665 $adamsEdit['wpSummary'] = 'Adam\'s edit';
666 $bertasEdit['wpSummary'] = 'Bertas\'s edit';
668 $adamsEdit['wpEdittime'] = $edittime;
669 $bertasEdit['wpEdittime'] = $edittime;
672 $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit,
673 EditPage
::AS_SUCCESS_UPDATE
, null, "expected successfull update" );
676 $this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
677 $expectedCode, $expectedText, $message );
681 * @depends testAutoMerge
683 public function testCheckDirectEditingDisallowed_forNonTextContent() {
684 $title = Title
::newFromText( 'Dummy:NonTextPageForEditPage' );
685 $page = WikiPage
::factory( $title );
687 $article = new Article( $title );
688 $article->getContext()->setTitle( $title );
689 $ep = new EditPage( $article );
690 $ep->setContextTitle( $title );
692 $user = $GLOBALS['wgUser'];
695 'wpTextbox1' => serialize( 'non-text content' ),
696 'wpEditToken' => $user->getEditToken(),
698 'wpStarttime' => wfTimestampNow(),
699 'wpUnicodeCheck' => EditPage
::UNICODE_CHECK
,
702 $req = new FauxRequest( $edit, true );
703 $ep->importFormData( $req );
705 $this->setExpectedException(
707 'This content model is not supported: testing'
710 $ep->internalAttemptSave( $result, false );