wiki.techinc.nl/tests/phpunit/includes/EditPageTest.php

1022 lines
28 KiB
PHP
Raw Normal View History

<?php
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Storage\EditResult;
use MediaWiki\User\UserIdentity;
use Wikimedia\TestingAccessWrapper;
2012-03-15 15:38:11 +00:00
/**
* @group Editing
*
* @group Database
* ^--- tell jenkins this test needs the database
*
* @group medium
* ^--- tell phpunit that these test cases may take longer than 2 seconds.
2012-03-15 15:38:11 +00:00
*/
class EditPageTest extends MediaWikiLangTestCase {
protected function setUp(): void {
parent::setUp();
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
$this->setContentLang( $contLang );
$this->setMwGlobals( [
'wgExtraNamespaces' => [
12312 => 'Dummy',
12313 => 'Dummy_talk',
],
'wgNamespaceContentModels' => [ 12312 => 'testing' ],
] );
$this->mergeMwGlobalArrayValue(
'wgContentHandlers',
[ 'testing' => 'DummyContentHandlerForTesting' ]
);
}
/**
Clean and repair many phpunit tests (+ fix implied configuration) This commit depends on the introduction of MediaWikiTestCase::setMwGlobals in change Iccf6ea81f4. Various tests already set their globals, but forgot to restore them afterwards, or forgot to call the parent setUp, tearDown... Either way they won't have to anymore with setMwGlobals. Consistent use of function characteristics: * protected function setUp * protected function tearDown * public static function (provide..) (Matching the function signature with PHPUnit/Framework/TestCase.php) Replaces: * public function (setUp|tearDown)\( * protected function $1( * \tfunction (setUp|tearDown)\( * \tprotected function $1( * \tfunction (data|provide)\( * \tpublic static function $1\( Also renamed a few "data#", "provider#" and "provides#" functions to "provide#" for consistency. This also removes confusion where the /media tests had a few private methods called dataFile(), which were sometimes expected to be data providers. Fixes: TimestampTest often failed due to a previous test setting a different language (it tests "1 hour ago" so need to make sure it is set to English). MWNamespaceTest became a lot cleaner now that it executes with a known context. Though the now-redundant code that was removed didn't work anyway because wgContentNamespaces isn't keyed by namespace id, it had them was values... FileBackendTest: * Fixed: "PHP Fatal: Using $this when not in object context" HttpTest * Added comment about: "PHP Fatal: Call to protected MWHttpRequest::__construct()" (too much unrelated code to fix in this commit) ExternalStoreTest * Add an assertTrue as well, without it the test is useless because regardless of whether wgExternalStores is true or false it only uses it if it is an array. Change-Id: I9d2b148e57bada64afeb7d5a99bec0e58f8e1561
2012-10-08 10:56:20 +00:00
* @dataProvider provideExtractSectionTitle
* @covers EditPage::extractSectionTitle
*/
public function testExtractSectionTitle( $section, $title ) {
$this->assertEquals(
$title,
TestingAccessWrapper::newFromClass( EditPage::class )->extractSectionTitle( $section )
);
}
Clean and repair many phpunit tests (+ fix implied configuration) This commit depends on the introduction of MediaWikiTestCase::setMwGlobals in change Iccf6ea81f4. Various tests already set their globals, but forgot to restore them afterwards, or forgot to call the parent setUp, tearDown... Either way they won't have to anymore with setMwGlobals. Consistent use of function characteristics: * protected function setUp * protected function tearDown * public static function (provide..) (Matching the function signature with PHPUnit/Framework/TestCase.php) Replaces: * public function (setUp|tearDown)\( * protected function $1( * \tfunction (setUp|tearDown)\( * \tprotected function $1( * \tfunction (data|provide)\( * \tpublic static function $1\( Also renamed a few "data#", "provider#" and "provides#" functions to "provide#" for consistency. This also removes confusion where the /media tests had a few private methods called dataFile(), which were sometimes expected to be data providers. Fixes: TimestampTest often failed due to a previous test setting a different language (it tests "1 hour ago" so need to make sure it is set to English). MWNamespaceTest became a lot cleaner now that it executes with a known context. Though the now-redundant code that was removed didn't work anyway because wgContentNamespaces isn't keyed by namespace id, it had them was values... FileBackendTest: * Fixed: "PHP Fatal: Using $this when not in object context" HttpTest * Added comment about: "PHP Fatal: Call to protected MWHttpRequest::__construct()" (too much unrelated code to fix in this commit) ExternalStoreTest * Add an assertTrue as well, without it the test is useless because regardless of whether wgExternalStores is true or false it only uses it if it is an array. Change-Id: I9d2b148e57bada64afeb7d5a99bec0e58f8e1561
2012-10-08 10:56:20 +00:00
public static function provideExtractSectionTitle() {
return [
[
"== Test ==\n\nJust a test section.",
"Test"
],
[
"An initial section, no header.",
false
],
[
"An initial section with a fake heder (T34617)\n\n== Test == ??\nwtf",
false
],
[
"== Section ==\nfollowed by a fake == Non-section == ??\nnoooo",
"Section"
],
[
"== Section== \t\r\n followed by whitespace (T37051)",
2012-03-15 15:38:11 +00:00
'Section',
],
];
}
protected function forceRevisionDate( WikiPage $page, $timestamp ) {
$dbw = wfGetDB( DB_PRIMARY );
$dbw->update( 'revision',
[ 'rev_timestamp' => $dbw->timestamp( $timestamp ) ],
[ 'rev_id' => $page->getLatest() ] );
$page->clear();
}
/**
* User input text is passed to rtrim() by edit page. This is a simple
* wrapper around assertEquals() which calls rrtrim() to normalize the
* expected and actual texts.
* @param string $expected
* @param string $actual
* @param string $msg
*/
protected function assertEditedTextEquals( $expected, $actual, $msg = '' ) {
$this->assertEquals( rtrim( $expected ), rtrim( $actual ), $msg );
}
/**
* Performs an edit and checks the result.
*
* @param string|Title $title The title of the page to edit
* @param string|null $baseText Some text to create the page with before attempting the edit.
* @param User|string|null $user The user to perform the edit as.
* @param array $edit An array of request parameters used to define the edit to perform.
* Some well known fields are:
* * wpTextbox1: the text to submit
* * wpSummary: the edit summary
* * wpEditToken: the edit token (will be inserted if not provided)
* * wpEdittime: timestamp of the edit's base revision (will be inserted
* if not provided)
* * editRevId: revision ID of the edit's base revision (optional)
* * wpStarttime: timestamp when the edit started (will be inserted if not provided)
* * wpSectionTitle: the section to edit
* * wpMinoredit: mark as minor edit
* * wpWatchthis: whether to watch the page
* @param int|null $expectedCode The expected result code (EditPage::AS_XXX constants).
* Set to null to skip the check.
* @param string|null $expectedText The text expected to be on the page after the edit.
* Set to null to skip the check.
* @param string|null $message An optional message to show along with any error message.
*
* @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
*/
protected function assertEdit( $title, $baseText, $user, array $edit,
$expectedCode = null, $expectedText = null, $message = null
) {
if ( is_string( $title ) ) {
$ns = $this->getDefaultWikitextNS();
$title = Title::newFromText( $title, $ns );
}
$this->assertNotNull( $title );
if ( is_string( $user ) ) {
$user = User::newFromName( $user );
if ( $user->getId() === 0 ) {
$user->addToDatabase();
}
}
if ( $user == null ) {
$user = $this->getTestUser()->getUser();
}
$page = WikiPage::factory( $title );
if ( $baseText !== null ) {
$content = ContentHandler::makeContent( $baseText, $title );
$page->doUserEditContent( $content, $user, "base text for test" );
$this->forceRevisionDate( $page, '20120101000000' );
// sanity check
$page->clear();
$currentText = ContentHandler::getContentText( $page->getContent() );
# EditPage rtrim() the user input, so we alter our expected text
# to reflect that.
$this->assertEditedTextEquals( $baseText, $currentText );
}
if ( !isset( $edit['wpEditToken'] ) ) {
$edit['wpEditToken'] = $user->getEditToken();
}
if ( !isset( $edit['wpEdittime'] ) && !isset( $edit['editRevId'] ) ) {
$edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : '';
}
if ( !isset( $edit['wpStarttime'] ) ) {
$edit['wpStarttime'] = wfTimestampNow();
}
if ( !isset( $edit['wpUnicodeCheck'] ) ) {
$edit['wpUnicodeCheck'] = EditPage::UNICODE_CHECK;
}
$req = new FauxRequest( $edit, true ); // session ??
$context = new RequestContext();
$context->setRequest( $req );
$context->setTitle( $title );
$context->setUser( $user );
$article = new Article( $title );
$article->setContext( $context );
$ep = new EditPage( $article );
$ep->setContextTitle( $title );
$ep->importFormData( $req );
// this is where the edit happens!
// Note: don't want to use EditPage::AttemptSave, because it messes with $wgOut
// and throws exceptions like PermissionsError
$status = $ep->attemptSave( $result );
if ( $expectedCode !== null ) {
// check edit code
$this->assertEquals( $expectedCode, $status->value,
"Expected result code mismatch. $message" );
}
$page = WikiPage::factory( $title );
if ( $expectedText !== null ) {
// check resulting page text
$content = $page->getContent();
$text = ContentHandler::getContentText( $content );
# EditPage rtrim() the user input, so we alter our expected text
# to reflect that.
$this->assertEditedTextEquals( $expectedText, $text,
"Expected article text mismatch. $message" );
}
return $page;
}
public static function provideCreatePages() {
return [
[ 'expected article being created',
'EditPageTest_testCreatePage',
null,
'Hello World!',
EditPage::AS_SUCCESS_NEW_ARTICLE,
'Hello World!'
],
[ 'expected article not being created if empty',
'EditPageTest_testCreatePage',
null,
'',
EditPage::AS_BLANK_ARTICLE,
null
],
[ 'expected MediaWiki: page being created',
'MediaWiki:January',
'UTSysop',
'Not January',
EditPage::AS_SUCCESS_NEW_ARTICLE,
'Not January'
],
[ 'expected not-registered MediaWiki: page not being created if empty',
'MediaWiki:EditPageTest_testCreatePage',
'UTSysop',
'',
EditPage::AS_BLANK_ARTICLE,
null
],
[ 'expected registered MediaWiki: page being created even if empty',
'MediaWiki:January',
'UTSysop',
'',
EditPage::AS_SUCCESS_NEW_ARTICLE,
''
],
[ 'expected registered MediaWiki: page whose default content is empty'
. ' not being created if empty',
'MediaWiki:Ipb-default-expiry',
'UTSysop',
'',
EditPage::AS_BLANK_ARTICLE,
''
],
[ 'expected MediaWiki: page not being created if text equals default message',
'MediaWiki:January',
'UTSysop',
'January',
EditPage::AS_BLANK_ARTICLE,
null
],
[ 'expected empty article being created',
'EditPageTest_testCreatePage',
null,
'',
EditPage::AS_SUCCESS_NEW_ARTICLE,
'',
true
],
];
}
/**
* @dataProvider provideCreatePages
* @covers EditPage
*/
public function testCreatePage(
$desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
) {
$checkId = null;
$this->setMwGlobals( 'wgHooks', [
'PageSaveComplete' => [ static function (
WikiPage $page, UserIdentity $user, string $summary,
int $flags, RevisionRecord $revisionRecord, EditResult $editResult
) use ( &$checkId ) {
$checkId = $revisionRecord->getId();
// types/refs checked
} ],
] );
$edit = [ 'wpTextbox1' => $editText ];
if ( $ignoreBlank ) {
$edit['wpIgnoreBlankArticle'] = 1;
}
$page = $this->assertEdit( $pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) {
$latest = $page->getLatest();
$page->doDeleteArticleReal( $pageTitle, $this->getTestSysop()->getUser() );
$this->assertGreaterThan( 0, $latest, "Page revision ID updated in object" );
$this->assertEquals( $latest, $checkId, "Revision in Status for hook" );
}
}
/**
* @dataProvider provideCreatePages
* @covers EditPage
*/
public function testCreatePageTrx(
$desc, $pageTitle, $user, $editText, $expectedCode, $expectedText, $ignoreBlank = false
) {
$checkIds = [];
$this->setMwGlobals( 'wgHooks', [
'PageSaveComplete' => [ static function (
WikiPage $page, UserIdentity $user, string $summary,
int $flags, RevisionRecord $revisionRecord, EditResult $editResult
) use ( &$checkIds ) {
$checkIds[] = $revisionRecord->getId();
// types/refs checked
} ],
] );
wfGetDB( DB_PRIMARY )->begin( __METHOD__ );
$edit = [ 'wpTextbox1' => $editText ];
if ( $ignoreBlank ) {
$edit['wpIgnoreBlankArticle'] = 1;
}
$page = $this->assertEdit(
$pageTitle, null, $user, $edit, $expectedCode, $expectedText, $desc );
$pageTitle2 = (string)$pageTitle . '/x';
$page2 = $this->assertEdit(
$pageTitle2, null, $user, $edit, $expectedCode, $expectedText, $desc );
wfGetDB( DB_PRIMARY )->commit( __METHOD__ );
$this->assertSame( 0, DeferredUpdates::pendingUpdatesCount(), 'No deferred updates' );
if ( $expectedCode != EditPage::AS_BLANK_ARTICLE ) {
$latest = $page->getLatest();
$page->doDeleteArticleReal( $pageTitle, $this->getTestSysop()->getUser() );
$this->assertGreaterThan( 0, $latest, "Page #1 revision ID updated in object" );
$this->assertEquals( $latest, $checkIds[0], "Revision #1 in Status for hook" );
$latest2 = $page2->getLatest();
$page2->doDeleteArticleReal( $pageTitle2, $this->getTestSysop()->getUser() );
$this->assertGreaterThan( 0, $latest2, "Page #2 revision ID updated in object" );
$this->assertEquals( $latest2, $checkIds[1], "Revision #2 in Status for hook" );
}
}
/**
* @covers EditPage
*/
public function testUpdatePage() {
$checkIds = [];
$this->setMwGlobals( 'wgHooks', [
'PageSaveComplete' => [ static function (
WikiPage $page, UserIdentity $user, string $summary,
int $flags, RevisionRecord $revisionRecord, EditResult $editResult
) use ( &$checkIds ) {
$checkIds[] = $revisionRecord->getId();
// types/refs checked
} ],
] );
$text = "one";
$edit = [
'wpTextbox1' => $text,
'wpSummary' => 'first update',
];
$page = $this->assertEdit( 'EditPageTest_testUpdatePage', "zero", null, $edit,
EditPage::AS_SUCCESS_UPDATE, $text,
"expected successful update with given text" );
$this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
$this->forceRevisionDate( $page, '20120101000000' );
$text = "two";
$edit = [
'wpTextbox1' => $text,
'wpSummary' => 'second update',
];
$this->assertEdit( 'EditPageTest_testUpdatePage', null, null, $edit,
EditPage::AS_SUCCESS_UPDATE, $text,
"expected successful update with given text" );
$this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
$this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
}
/**
* @covers EditPage
*/
public function testUpdateNoMinor() {
$user = $this->getTestUser()->getUser();
$anon = new User(); // anon
// Test that page creation can never be minor
$edit = [
'wpTextbox1' => 'testing',
'wpSummary' => 'first update',
'wpMinoredit' => 'minor'
];
$page = $this->assertEdit( 'EditPageTest_testUpdateNoMinor', null, $user, $edit,
EditPage::AS_SUCCESS_NEW_ARTICLE, 'testing', "expected successful update" );
$this->assertFalse(
$page->getRevisionRecord()->isMinor(),
'page creation should not be minor'
);
// Test that anons can't make an update minor
$this->forceRevisionDate( $page, '20120101000000' );
$edit = [
'wpTextbox1' => 'testing 2',
'wpSummary' => 'second update',
'wpMinoredit' => 'minor'
];
$page = $this->assertEdit( 'EditPageTest_testUpdateNoMinor', null, $anon, $edit,
EditPage::AS_SUCCESS_UPDATE, 'testing 2', "expected successful update" );
$this->assertFalse(
$page->getRevisionRecord()->isMinor(),
'anon edit should not be minor'
);
// Test that users can make an update minor
$this->forceRevisionDate( $page, '20120102000000' );
$edit = [
'wpTextbox1' => 'testing 3',
'wpSummary' => 'third update',
'wpMinoredit' => 'minor'
];
$page = $this->assertEdit( 'EditPageTest_testUpdateNoMinor', null, $user, $edit,
EditPage::AS_SUCCESS_UPDATE, 'testing 3', "expected successful update" );
$this->assertTrue(
$page->getRevisionRecord()->isMinor(),
'users can make edits minor'
);
}
/**
* @covers EditPage
*/
public function testUpdatePageTrx() {
$text = "one";
$edit = [
'wpTextbox1' => $text,
'wpSummary' => 'first update',
];
$page = $this->assertEdit( 'EditPageTest_testTrxUpdatePage', "zero", null, $edit,
EditPage::AS_SUCCESS_UPDATE, $text,
"expected successful update with given text" );
$this->forceRevisionDate( $page, '20120101000000' );
$checkIds = [];
$this->setMwGlobals( 'wgHooks', [
'PageSaveComplete' => [ static function (
WikiPage $page, UserIdentity $user, string $summary,
int $flags, RevisionRecord $revisionRecord, EditResult $editResult
) use ( &$checkIds ) {
$checkIds[] = $revisionRecord->getId();
// types/refs checked
} ],
] );
wfGetDB( DB_PRIMARY )->begin( __METHOD__ );
$text = "two";
$edit = [
'wpTextbox1' => $text,
'wpSummary' => 'second update',
];
$this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
EditPage::AS_SUCCESS_UPDATE, $text,
"expected successful update with given text" );
$text = "three";
$edit = [
'wpTextbox1' => $text,
'wpSummary' => 'third update',
];
$this->assertEdit( 'EditPageTest_testTrxUpdatePage', null, null, $edit,
EditPage::AS_SUCCESS_UPDATE, $text,
"expected successful update with given text" );
wfGetDB( DB_PRIMARY )->commit( __METHOD__ );
$this->assertGreaterThan( 0, $checkIds[0], "First event rev ID set" );
$this->assertGreaterThan( 0, $checkIds[1], "Second edit hook rev ID set" );
$this->assertGreaterThan( $checkIds[0], $checkIds[1], "Second event rev ID is higher" );
}
public static function provideSectionEdit() {
Disable section editing on non-current revision. Production error reported in T161199 was caused because of some bug on how section edit support is checked in EditPage. When we attemt to edit a section of any page; isSectionEditSupported() method is called to check whether the content model allows that. If it doesn't, an ErrorPageError is thrown and that's the end. However, the method flaw is that it only checks the content model of the page's title which is equivalent to the model of the latest revision. But ealier page revisions now may contain different content models from the latest revision. If a page's current CM ALLOWS section edit and one of its older revisions has a CM that does NOT ALLOW section edit, attempting to edit any section of that old revision will lead to a call to the isSectionEditSupported(), which will now return incorrect truthy result based on the current page's content model that allows section edit. This will lead to a call to getOriginalContent() which returns content of the revision. Attempting to extract content of a section from that will return null and the null is retained as the return value of getContentObject(). Since the old revision and current revision have different CMs edit will not be llowed, to explain this, a call will be made to displayViewSourcePage() method which is typehinted to require Content as the as its first argument. So because of the bug null will be given to displayViewSourcePage() instead of Content and that will lead to the TypeError that was reported in T161199. The initial and simple solution to this bug is to just fix the method to ensure that it checks both content models of the title and the relevant fetched revision which will ensure correct result for section edit support is returned. But later it was pointed out by Anomie that editing section of oldrevision is not something that was intentinally designed to be so. In fact it was disallowed from the UI because of its consequence, but it was not enforced technically. Task T251680 was created to explain this in more depth. This patch; * Section edit support is changed to require, in addition to the content model allowing so, that the revision to be edited be current revision. * Section editing on non-current revision is already superficially disallowed as there's UI button to reach that patch. Even the initial reported bug couldn't have happened or be reproduced any UI means except by directly playing with the URL * This will automatically resolve the original bug that led to the error and enforce what has been already disallowed. Bug: T251680 Bug: T161199 Change-Id: I05ca6ac90509deec51bf38ec98e9aef169d87459
2020-03-30 10:39:53 +00:00
$title = 'EditPageTest_testSectionEdit';
$title2 = Title::newFromText( __FUNCTION__ );
$title2->setContentModel( CONTENT_MODEL_CSS );
$text = 'Intro
== one ==
first section.
== two ==
second section.
';
$sectionOne = '== one ==
hello
';
$newSection = '== new section ==
hello
';
$textWithNewSectionOne = preg_replace(
'/== one ==.*== two ==/ms',
"$sectionOne\n== two ==", $text
);
$textWithNewSectionAdded = "$text\n$newSection";
return [
[ # 0
Disable section editing on non-current revision. Production error reported in T161199 was caused because of some bug on how section edit support is checked in EditPage. When we attemt to edit a section of any page; isSectionEditSupported() method is called to check whether the content model allows that. If it doesn't, an ErrorPageError is thrown and that's the end. However, the method flaw is that it only checks the content model of the page's title which is equivalent to the model of the latest revision. But ealier page revisions now may contain different content models from the latest revision. If a page's current CM ALLOWS section edit and one of its older revisions has a CM that does NOT ALLOW section edit, attempting to edit any section of that old revision will lead to a call to the isSectionEditSupported(), which will now return incorrect truthy result based on the current page's content model that allows section edit. This will lead to a call to getOriginalContent() which returns content of the revision. Attempting to extract content of a section from that will return null and the null is retained as the return value of getContentObject(). Since the old revision and current revision have different CMs edit will not be llowed, to explain this, a call will be made to displayViewSourcePage() method which is typehinted to require Content as the as its first argument. So because of the bug null will be given to displayViewSourcePage() instead of Content and that will lead to the TypeError that was reported in T161199. The initial and simple solution to this bug is to just fix the method to ensure that it checks both content models of the title and the relevant fetched revision which will ensure correct result for section edit support is returned. But later it was pointed out by Anomie that editing section of oldrevision is not something that was intentinally designed to be so. In fact it was disallowed from the UI because of its consequence, but it was not enforced technically. Task T251680 was created to explain this in more depth. This patch; * Section edit support is changed to require, in addition to the content model allowing so, that the revision to be edited be current revision. * Section editing on non-current revision is already superficially disallowed as there's UI button to reach that patch. Even the initial reported bug couldn't have happened or be reproduced any UI means except by directly playing with the URL * This will automatically resolve the original bug that led to the error and enforce what has been already disallowed. Bug: T251680 Bug: T161199 Change-Id: I05ca6ac90509deec51bf38ec98e9aef169d87459
2020-03-30 10:39:53 +00:00
$title,
$text,
'',
'hello',
'replace all',
'hello'
],
[ # 1
Disable section editing on non-current revision. Production error reported in T161199 was caused because of some bug on how section edit support is checked in EditPage. When we attemt to edit a section of any page; isSectionEditSupported() method is called to check whether the content model allows that. If it doesn't, an ErrorPageError is thrown and that's the end. However, the method flaw is that it only checks the content model of the page's title which is equivalent to the model of the latest revision. But ealier page revisions now may contain different content models from the latest revision. If a page's current CM ALLOWS section edit and one of its older revisions has a CM that does NOT ALLOW section edit, attempting to edit any section of that old revision will lead to a call to the isSectionEditSupported(), which will now return incorrect truthy result based on the current page's content model that allows section edit. This will lead to a call to getOriginalContent() which returns content of the revision. Attempting to extract content of a section from that will return null and the null is retained as the return value of getContentObject(). Since the old revision and current revision have different CMs edit will not be llowed, to explain this, a call will be made to displayViewSourcePage() method which is typehinted to require Content as the as its first argument. So because of the bug null will be given to displayViewSourcePage() instead of Content and that will lead to the TypeError that was reported in T161199. The initial and simple solution to this bug is to just fix the method to ensure that it checks both content models of the title and the relevant fetched revision which will ensure correct result for section edit support is returned. But later it was pointed out by Anomie that editing section of oldrevision is not something that was intentinally designed to be so. In fact it was disallowed from the UI because of its consequence, but it was not enforced technically. Task T251680 was created to explain this in more depth. This patch; * Section edit support is changed to require, in addition to the content model allowing so, that the revision to be edited be current revision. * Section editing on non-current revision is already superficially disallowed as there's UI button to reach that patch. Even the initial reported bug couldn't have happened or be reproduced any UI means except by directly playing with the URL * This will automatically resolve the original bug that led to the error and enforce what has been already disallowed. Bug: T251680 Bug: T161199 Change-Id: I05ca6ac90509deec51bf38ec98e9aef169d87459
2020-03-30 10:39:53 +00:00
$title,
$text,
'1',
$sectionOne,
'replace first section',
$textWithNewSectionOne,
],
[ # 2
Disable section editing on non-current revision. Production error reported in T161199 was caused because of some bug on how section edit support is checked in EditPage. When we attemt to edit a section of any page; isSectionEditSupported() method is called to check whether the content model allows that. If it doesn't, an ErrorPageError is thrown and that's the end. However, the method flaw is that it only checks the content model of the page's title which is equivalent to the model of the latest revision. But ealier page revisions now may contain different content models from the latest revision. If a page's current CM ALLOWS section edit and one of its older revisions has a CM that does NOT ALLOW section edit, attempting to edit any section of that old revision will lead to a call to the isSectionEditSupported(), which will now return incorrect truthy result based on the current page's content model that allows section edit. This will lead to a call to getOriginalContent() which returns content of the revision. Attempting to extract content of a section from that will return null and the null is retained as the return value of getContentObject(). Since the old revision and current revision have different CMs edit will not be llowed, to explain this, a call will be made to displayViewSourcePage() method which is typehinted to require Content as the as its first argument. So because of the bug null will be given to displayViewSourcePage() instead of Content and that will lead to the TypeError that was reported in T161199. The initial and simple solution to this bug is to just fix the method to ensure that it checks both content models of the title and the relevant fetched revision which will ensure correct result for section edit support is returned. But later it was pointed out by Anomie that editing section of oldrevision is not something that was intentinally designed to be so. In fact it was disallowed from the UI because of its consequence, but it was not enforced technically. Task T251680 was created to explain this in more depth. This patch; * Section edit support is changed to require, in addition to the content model allowing so, that the revision to be edited be current revision. * Section editing on non-current revision is already superficially disallowed as there's UI button to reach that patch. Even the initial reported bug couldn't have happened or be reproduced any UI means except by directly playing with the URL * This will automatically resolve the original bug that led to the error and enforce what has been already disallowed. Bug: T251680 Bug: T161199 Change-Id: I05ca6ac90509deec51bf38ec98e9aef169d87459
2020-03-30 10:39:53 +00:00
$title,
$text,
'new',
'hello',
'new section',
$textWithNewSectionAdded,
],
Disable section editing on non-current revision. Production error reported in T161199 was caused because of some bug on how section edit support is checked in EditPage. When we attemt to edit a section of any page; isSectionEditSupported() method is called to check whether the content model allows that. If it doesn't, an ErrorPageError is thrown and that's the end. However, the method flaw is that it only checks the content model of the page's title which is equivalent to the model of the latest revision. But ealier page revisions now may contain different content models from the latest revision. If a page's current CM ALLOWS section edit and one of its older revisions has a CM that does NOT ALLOW section edit, attempting to edit any section of that old revision will lead to a call to the isSectionEditSupported(), which will now return incorrect truthy result based on the current page's content model that allows section edit. This will lead to a call to getOriginalContent() which returns content of the revision. Attempting to extract content of a section from that will return null and the null is retained as the return value of getContentObject(). Since the old revision and current revision have different CMs edit will not be llowed, to explain this, a call will be made to displayViewSourcePage() method which is typehinted to require Content as the as its first argument. So because of the bug null will be given to displayViewSourcePage() instead of Content and that will lead to the TypeError that was reported in T161199. The initial and simple solution to this bug is to just fix the method to ensure that it checks both content models of the title and the relevant fetched revision which will ensure correct result for section edit support is returned. But later it was pointed out by Anomie that editing section of oldrevision is not something that was intentinally designed to be so. In fact it was disallowed from the UI because of its consequence, but it was not enforced technically. Task T251680 was created to explain this in more depth. This patch; * Section edit support is changed to require, in addition to the content model allowing so, that the revision to be edited be current revision. * Section editing on non-current revision is already superficially disallowed as there's UI button to reach that patch. Even the initial reported bug couldn't have happened or be reproduced any UI means except by directly playing with the URL * This will automatically resolve the original bug that led to the error and enforce what has been already disallowed. Bug: T251680 Bug: T161199 Change-Id: I05ca6ac90509deec51bf38ec98e9aef169d87459
2020-03-30 10:39:53 +00:00
[ # 3 Section edit not supported
$title2,
$text,
'1',
'hello',
'',
'',
],
];
}
/**
* @dataProvider provideSectionEdit
* @covers EditPage
*/
Disable section editing on non-current revision. Production error reported in T161199 was caused because of some bug on how section edit support is checked in EditPage. When we attemt to edit a section of any page; isSectionEditSupported() method is called to check whether the content model allows that. If it doesn't, an ErrorPageError is thrown and that's the end. However, the method flaw is that it only checks the content model of the page's title which is equivalent to the model of the latest revision. But ealier page revisions now may contain different content models from the latest revision. If a page's current CM ALLOWS section edit and one of its older revisions has a CM that does NOT ALLOW section edit, attempting to edit any section of that old revision will lead to a call to the isSectionEditSupported(), which will now return incorrect truthy result based on the current page's content model that allows section edit. This will lead to a call to getOriginalContent() which returns content of the revision. Attempting to extract content of a section from that will return null and the null is retained as the return value of getContentObject(). Since the old revision and current revision have different CMs edit will not be llowed, to explain this, a call will be made to displayViewSourcePage() method which is typehinted to require Content as the as its first argument. So because of the bug null will be given to displayViewSourcePage() instead of Content and that will lead to the TypeError that was reported in T161199. The initial and simple solution to this bug is to just fix the method to ensure that it checks both content models of the title and the relevant fetched revision which will ensure correct result for section edit support is returned. But later it was pointed out by Anomie that editing section of oldrevision is not something that was intentinally designed to be so. In fact it was disallowed from the UI because of its consequence, but it was not enforced technically. Task T251680 was created to explain this in more depth. This patch; * Section edit support is changed to require, in addition to the content model allowing so, that the revision to be edited be current revision. * Section editing on non-current revision is already superficially disallowed as there's UI button to reach that patch. Even the initial reported bug couldn't have happened or be reproduced any UI means except by directly playing with the URL * This will automatically resolve the original bug that led to the error and enforce what has been already disallowed. Bug: T251680 Bug: T161199 Change-Id: I05ca6ac90509deec51bf38ec98e9aef169d87459
2020-03-30 10:39:53 +00:00
public function testSectionEdit( $title, $base, $section, $text, $summary, $expected ) {
$edit = [
'wpTextbox1' => $text,
'wpSummary' => $summary,
'wpSection' => $section,
];
Disable section editing on non-current revision. Production error reported in T161199 was caused because of some bug on how section edit support is checked in EditPage. When we attemt to edit a section of any page; isSectionEditSupported() method is called to check whether the content model allows that. If it doesn't, an ErrorPageError is thrown and that's the end. However, the method flaw is that it only checks the content model of the page's title which is equivalent to the model of the latest revision. But ealier page revisions now may contain different content models from the latest revision. If a page's current CM ALLOWS section edit and one of its older revisions has a CM that does NOT ALLOW section edit, attempting to edit any section of that old revision will lead to a call to the isSectionEditSupported(), which will now return incorrect truthy result based on the current page's content model that allows section edit. This will lead to a call to getOriginalContent() which returns content of the revision. Attempting to extract content of a section from that will return null and the null is retained as the return value of getContentObject(). Since the old revision and current revision have different CMs edit will not be llowed, to explain this, a call will be made to displayViewSourcePage() method which is typehinted to require Content as the as its first argument. So because of the bug null will be given to displayViewSourcePage() instead of Content and that will lead to the TypeError that was reported in T161199. The initial and simple solution to this bug is to just fix the method to ensure that it checks both content models of the title and the relevant fetched revision which will ensure correct result for section edit support is returned. But later it was pointed out by Anomie that editing section of oldrevision is not something that was intentinally designed to be so. In fact it was disallowed from the UI because of its consequence, but it was not enforced technically. Task T251680 was created to explain this in more depth. This patch; * Section edit support is changed to require, in addition to the content model allowing so, that the revision to be edited be current revision. * Section editing on non-current revision is already superficially disallowed as there's UI button to reach that patch. Even the initial reported bug couldn't have happened or be reproduced any UI means except by directly playing with the URL * This will automatically resolve the original bug that led to the error and enforce what has been already disallowed. Bug: T251680 Bug: T161199 Change-Id: I05ca6ac90509deec51bf38ec98e9aef169d87459
2020-03-30 10:39:53 +00:00
$msg = "expected successful update of section";
$result = EditPage::AS_SUCCESS_UPDATE;
if ( $title instanceof Title ) {
$result = null;
$this->expectException( ErrorPageError::class );
}
$this->assertEdit( $title, $base, null, $edit, $result, $expected, $msg );
}
public static function provideConflictDetection() {
yield 'no conflict detected' => [
'Adam',
[
'wpEdittime' => 2, // use the second edit's time
'editRevId' => 2, // use the second edit's revision ID
],
EditPage::AS_SUCCESS_UPDATE,
'successful update expected'
];
yield 'conflict detected based on wpEdittime' => [
'Adam',
[
'wpEdittime' => 1, // use the first edit's time
],
EditPage::AS_CONFLICT_DETECTED,
'conflict expected'
];
yield 'conflict detected based on editRevId' => [
'Adam',
[
'editRevId' => 1, // use the first edit's revision ID
],
EditPage::AS_CONFLICT_DETECTED,
'conflict expected'
];
yield 'conflict based on wpEdittime ignored for same user' => [
'Berta',
[
'wpEdittime' => 1, // use the first edit's time
],
EditPage::AS_SUCCESS_UPDATE,
'successful update expected'
];
yield 'conflict detected based on editRevId even for same user' => [
'Berta',
[
'editRevId' => 1, // use the first edit's revision ID
],
EditPage::AS_CONFLICT_DETECTED,
'conflict expected'
];
}
/**
* @dataProvider provideConflictDetection
* @covers EditPage
*/
public function testConflictDetection( $editUser, $newEdit, $expectedCode, $message ) {
// create page
$ns = $this->getDefaultWikitextNS();
$title = Title::newFromText( __METHOD__, $ns );
$page = WikiPage::factory( $title );
if ( $page->exists() ) {
$page->doDeleteArticleReal(
"clean slate for testing",
$this->getTestSysop()->getUser()
);
}
$elmosEdit['wpTextbox1'] = 'Elmo\'s text';
$bertasEdit['wpTextbox1'] = 'Berta\'s text';
$newEdit['wpTextbox1'] = 'new text';
$elmosEdit['wpSummary'] = 'Elmo\'s edit';
$bertasEdit['wpSummary'] = 'Bertas\'s edit';
$newEdit['wpSummary'] = $newEdit['wpSummary'] ?? 'new edit';
// first edit: Elmo
$page = $this->assertEdit( __METHOD__, null, 'Elmo', $elmosEdit,
EditPage::AS_SUCCESS_NEW_ARTICLE, null, 'expected successful creation' );
$this->forceRevisionDate( $page, '20120101000000' );
$rev1 = $page->getRevisionRecord();
// second edit: Berta
$page = $this->assertEdit( __METHOD__, null, 'Berta', $bertasEdit,
EditPage::AS_SUCCESS_UPDATE, null, 'expected successful update' );
$this->forceRevisionDate( $page, '20120101111111' );
$rev2 = $page->getRevisionRecord();
if ( !empty( $newEdit['editRevId'] ) ) {
$newEdit['editRevId'] = $newEdit['editRevId'] === 1 ? $rev1->getId() : $rev2->getId();
}
if ( !empty( $newEdit['wpEdittime'] ) ) {
$newEdit['wpEdittime'] =
$newEdit['wpEdittime'] === 1 ? $rev1->getTimestamp() : $rev2->getTimestamp();
}
// third edit
$this->assertEdit( __METHOD__, null, $editUser, $newEdit,
$expectedCode, null, $message );
}
public static function provideAutoMerge() {
$tests = [];
$tests[] = [ # 0: plain conflict
"Elmo", # base edit user
"one\n\ntwo\n\nthree\n",
[ # adam's edit
'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
],
[ # berta's edit
'wpTextbox1' => "(one)\n\ntwo\n\nthree\n",
],
EditPage::AS_CONFLICT_DETECTED, # expected code
"ONE\n\ntwo\n\nthree\n", # expected text
'expected edit conflict', # message
];
$tests[] = [ # 1: successful merge
"Elmo", # base edit user
"one\n\ntwo\n\nthree\n",
[ # adam's edit
'wpStarttime' => 1,
'wpTextbox1' => "ONE\n\ntwo\n\nthree\n",
],
[ # berta's edit
'wpStarttime' => 2,
'wpTextbox1' => "one\n\ntwo\n\nTHREE\n",
],
EditPage::AS_SUCCESS_UPDATE, # expected code
"ONE\n\ntwo\n\nTHREE\n", # expected text
'expected automatic merge', # message
];
$text = "Intro\n\n";
$text .= "== first section ==\n\n";
$text .= "one\n\ntwo\n\nthree\n\n";
$text .= "== second section ==\n\n";
$text .= "four\n\nfive\n\nsix\n\n";
// extract the first section.
$section = preg_replace( '/.*(== first section ==.*)== second section ==.*/sm', '$1', $text );
// generate expected text after merge
$expected = str_replace( 'one', 'ONE', str_replace( 'three', 'THREE', $text ) );
$tests[] = [ # 2: merge in section
"Elmo", # base edit user
$text,
[ # adam's edit
'wpTextbox1' => str_replace( 'one', 'ONE', $section ),
'wpSection' => '1'
],
[ # berta's edit
'wpTextbox1' => str_replace( 'three', 'THREE', $section ),
'wpSection' => '1'
],
EditPage::AS_SUCCESS_UPDATE, # expected code
$expected, # expected text
'expected automatic section merge', # message
];
// see whether it makes a difference who did the base edit
$testsWithAdam = array_map( static function ( $test ) {
$test[0] = 'Adam'; // change base edit user
return $test;
}, $tests );
$testsWithBerta = array_map( static function ( $test ) {
$test[0] = 'Berta'; // change base edit user
return $test;
}, $tests );
return array_merge( $tests, $testsWithAdam, $testsWithBerta );
}
/**
* @dataProvider provideAutoMerge
* @covers EditPage
*/
public function testAutoMerge( $baseUser, $text, $adamsEdit, $bertasEdit,
$expectedCode, $expectedText, $message = null
) {
$this->markTestSkippedIfNoDiff3();
// create page
$ns = $this->getDefaultWikitextNS();
$title = Title::newFromText( 'EditPageTest_testAutoMerge', $ns );
$page = WikiPage::factory( $title );
if ( $page->exists() ) {
$page->doDeleteArticleReal(
"clean slate for testing",
$this->getTestSysop()->getUser()
);
}
$baseEdit = [
'wpTextbox1' => $text,
];
$page = $this->assertEdit( 'EditPageTest_testAutoMerge', null,
$baseUser, $baseEdit, null, null, __METHOD__ );
$this->forceRevisionDate( $page, '20120101000000' );
$edittime = $page->getTimestamp();
$revId = $page->getLatest();
$adamsEdit['wpSummary'] = 'Adam\'s edit';
$bertasEdit['wpSummary'] = 'Bertas\'s edit';
$adamsEdit['wpEdittime'] = $edittime;
$bertasEdit['wpEdittime'] = $edittime;
$adamsEdit['editRevId'] = $revId;
$bertasEdit['editRevId'] = $revId;
// first edit
$this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Adam', $adamsEdit,
EditPage::AS_SUCCESS_UPDATE, null, "expected successful update" );
// second edit
$this->assertEdit( 'EditPageTest_testAutoMerge', null, 'Berta', $bertasEdit,
$expectedCode, $expectedText, $message );
}
/**
* @depends testAutoMerge
* @covers EditPage
*/
public function testCheckDirectEditingDisallowed_forNonTextContent() {
$user = $this->getTestUser()->getUser();
$edit = [
'wpTextbox1' => serialize( 'non-text content' ),
'wpEditToken' => $user->getEditToken(),
'wpEdittime' => '',
'editRevId' => 0,
'wpStarttime' => wfTimestampNow(),
'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
];
$this->expectException( MWException::class );
$this->expectExceptionMessage( 'This content model is not supported: testing' );
$this->doEditDummyNonTextPage( $edit );
}
/** @covers EditPage */
public function testShouldPreventChangingContentModelWhenUserCannotChangeModelForTitle() {
$this->setTemporaryHook( 'getUserPermissionsErrors',
static function ( Title $page, $user, $action, &$result ) {
if ( $action === 'editcontentmodel' &&
$page->getContentModel() === CONTENT_MODEL_WIKITEXT
) {
$result = false;
return false;
}
} );
$user = $this->getTestUser()->getUser();
$status = $this->doEditDummyNonTextPage( [
'wpTextbox1' => 'some text',
'wpEditToken' => $user->getEditToken(),
'wpEdittime' => '',
'editRevId' => 0,
'wpStarttime' => wfTimestampNow(),
'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
'model' => CONTENT_MODEL_WIKITEXT,
'format' => CONTENT_FORMAT_WIKITEXT,
] );
$this->assertFalse( $status->isOK() );
$this->assertEquals( EditPage::AS_NO_CHANGE_CONTENT_MODEL, $status->getValue() );
}
/** @covers EditPage */
public function testShouldPreventChangingContentModelWhenUserCannotEditTargetTitle() {
$this->setTemporaryHook( 'getUserPermissionsErrors',
static function ( Title $page, $user, $action, &$result ) {
if ( $action === 'edit' && $page->getContentModel() === CONTENT_MODEL_WIKITEXT ) {
$result = false;
return false;
}
} );
$user = $this->getTestUser()->getUser();
$status = $this->doEditDummyNonTextPage( [
'wpTextbox1' => 'some text',
'wpEditToken' => $user->getEditToken(),
'wpEdittime' => '',
'editRevId' => 0,
'wpStarttime' => wfTimestampNow(),
'wpUnicodeCheck' => EditPage::UNICODE_CHECK,
'model' => CONTENT_MODEL_WIKITEXT,
'format' => CONTENT_FORMAT_WIKITEXT,
] );
$this->assertFalse( $status->isOK() );
$this->assertEquals( EditPage::AS_NO_CHANGE_CONTENT_MODEL, $status->getValue() );
}
private function doEditDummyNonTextPage( array $edit ): Status {
$title = Title::newFromText( 'Dummy:NonTextPageForEditPage' );
$article = new Article( $title );
$article->getContext()->setTitle( $title );
$ep = new EditPage( $article );
$ep->setContextTitle( $title );
$req = new FauxRequest( $edit, true );
$ep->importFormData( $req );
return $ep->internalAttemptSave( $result, false );
}
/**
* The watchlist expiry field should select the entered value on preview, rather than the
* calculated number of days till the expiry (as it shows on edit).
* @covers EditPage::getCheckboxesDefinition()
* @dataProvider provideWatchlistExpiry()
*/
public function testWatchlistExpiry( $existingExpiry, $postVal, $selected, $options ) {
// Set up config and fake current time.
$this->setMwGlobals( 'wgWatchlistExpiry', true );
MWTimestamp::setFakeTime( '20200505120000' );
$user = $this->getTestUser()->getUser();
$this->assertTrue( $user->isRegistered() );
// Create the EditPage.
$title = Title::newFromText( __METHOD__ );
$context = new RequestContext();
$context->setUser( $user );
$context->setTitle( $title );
$article = new Article( $title );
$article->setContext( $context );
$ep = new EditPage( $article );
$this->getServiceContainer()->getWatchlistManager()
->setWatch( (bool)$existingExpiry, $user, $title, $existingExpiry );
// Send the request.
$req = new FauxRequest( [ 'wpWatchlistExpiry' => $postVal ], true );
$context->setRequest( $req );
$req->getSession()->setUser( $user );
$ep->importFormData( $req );
$def = $ep->getCheckboxesDefinition( [ 'watch' => true ] )['wpWatchlistExpiry'];
// Test selected and available options.
$this->assertSame( $selected, $def['default'] );
$dropdownOptions = [];
foreach ( $def['options'] as $option ) {
// Reformat dropdown options for easier test comparison.
$dropdownOptions[] = $option['data'];
}
$this->assertSame( $options, $dropdownOptions );
}
public function provideWatchlistExpiry() {
$standardOptions = [ 'infinite', '1 week', '1 month', '3 months', '6 months' ];
return [
'not watched, request nothing' => [
'existingExpiry' => '',
'postVal' => '',
'selected' => 'infinite',
'options' => $standardOptions,
],
'not watched' => [
'existingExpiry' => '',
'postVal' => '1 month',
'result' => '1 month',
'options' => $standardOptions,
],
'watched with current selected' => [
'existingExpiry' => '2020-05-05T12:00:01Z',
'postVal' => '2020-05-05T12:00:01Z',
'result' => '2020-05-05T12:00:01Z',
'options' => array_merge( [ '2020-05-05T12:00:01Z' ], $standardOptions ),
],
'watched with 1 week selected' => [
'existingExpiry' => '2020-05-05T12:00:02Z',
'postVal' => '1 week',
'result' => '1 week',
'options' => array_merge( [ '2020-05-05T12:00:02Z' ], $standardOptions ),
],
];
}
Disable section editing on non-current revision. Production error reported in T161199 was caused because of some bug on how section edit support is checked in EditPage. When we attemt to edit a section of any page; isSectionEditSupported() method is called to check whether the content model allows that. If it doesn't, an ErrorPageError is thrown and that's the end. However, the method flaw is that it only checks the content model of the page's title which is equivalent to the model of the latest revision. But ealier page revisions now may contain different content models from the latest revision. If a page's current CM ALLOWS section edit and one of its older revisions has a CM that does NOT ALLOW section edit, attempting to edit any section of that old revision will lead to a call to the isSectionEditSupported(), which will now return incorrect truthy result based on the current page's content model that allows section edit. This will lead to a call to getOriginalContent() which returns content of the revision. Attempting to extract content of a section from that will return null and the null is retained as the return value of getContentObject(). Since the old revision and current revision have different CMs edit will not be llowed, to explain this, a call will be made to displayViewSourcePage() method which is typehinted to require Content as the as its first argument. So because of the bug null will be given to displayViewSourcePage() instead of Content and that will lead to the TypeError that was reported in T161199. The initial and simple solution to this bug is to just fix the method to ensure that it checks both content models of the title and the relevant fetched revision which will ensure correct result for section edit support is returned. But later it was pointed out by Anomie that editing section of oldrevision is not something that was intentinally designed to be so. In fact it was disallowed from the UI because of its consequence, but it was not enforced technically. Task T251680 was created to explain this in more depth. This patch; * Section edit support is changed to require, in addition to the content model allowing so, that the revision to be edited be current revision. * Section editing on non-current revision is already superficially disallowed as there's UI button to reach that patch. Even the initial reported bug couldn't have happened or be reproduced any UI means except by directly playing with the URL * This will automatically resolve the original bug that led to the error and enforce what has been already disallowed. Bug: T251680 Bug: T161199 Change-Id: I05ca6ac90509deec51bf38ec98e9aef169d87459
2020-03-30 10:39:53 +00:00
/**
* T277204
* @covers EditPage
*/
public function testFalseyEditRevId() {
$elmosEdit['wpTextbox1'] = 'Elmo\'s text';
$bertasEdit['wpTextbox1'] = 'Berta\'s text';
$elmosEdit['wpSummary'] = 'Elmo\'s edit';
$bertasEdit['wpSummary'] = 'Bertas\'s edit';
$bertasEdit['editRevId'] = 0;
$this->assertEdit( __METHOD__,
null, 'Elmo', $elmosEdit,
EditPage::AS_SUCCESS_NEW_ARTICLE, null, 'expected successful creation' );
// A successful update would probably be OK too. The important thing is
// that it doesn't throw an exception.
$this->assertEdit( __METHOD__, null, 'Berta', $bertasEdit,
EditPage::AS_CONFLICT_DETECTED, null, 'expected successful update' );
}
}