2020-10-23 03:17:31 +00:00
|
|
|
<?php
|
|
|
|
|
|
2023-02-13 19:56:51 +00:00
|
|
|
use MediaWiki\EditPage\EditPage;
|
2020-10-23 03:17:31 +00:00
|
|
|
use MediaWiki\EditPage\SpamChecker;
|
2022-07-15 00:07:38 +00:00
|
|
|
use MediaWiki\MainConfigNames;
|
2020-10-23 03:17:31 +00:00
|
|
|
use MediaWiki\Permissions\PermissionManager;
|
2022-10-28 10:04:25 +00:00
|
|
|
use MediaWiki\Request\FauxRequest;
|
2023-03-01 20:33:26 +00:00
|
|
|
use MediaWiki\Title\Title;
|
2023-05-04 21:41:21 +00:00
|
|
|
use Wikimedia\Rdbms\ReadOnlyMode;
|
2020-10-23 03:17:31 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Integration tests for the various edit constraints, ensuring
|
|
|
|
|
* that they result in failures as expected
|
|
|
|
|
*
|
2023-05-27 09:43:12 +00:00
|
|
|
* @covers MediaWiki\EditPage\EditPage::internalAttemptSave
|
2020-10-23 03:17:31 +00:00
|
|
|
*
|
|
|
|
|
* @group Editing
|
|
|
|
|
* @group Database
|
|
|
|
|
* @group medium
|
|
|
|
|
*/
|
|
|
|
|
class EditPageConstraintsTest extends MediaWikiLangTestCase {
|
|
|
|
|
|
2021-07-22 03:11:47 +00:00
|
|
|
protected function setUp(): void {
|
2020-10-23 03:17:31 +00:00
|
|
|
parent::setUp();
|
|
|
|
|
|
2022-07-15 00:07:38 +00:00
|
|
|
$contLang = $this->getServiceContainer()->getContentLanguage();
|
|
|
|
|
$this->overrideConfigValues( [
|
|
|
|
|
MainConfigNames::ExtraNamespaces => [
|
2020-10-23 03:17:31 +00:00
|
|
|
12312 => 'Dummy',
|
|
|
|
|
12313 => 'Dummy_talk',
|
|
|
|
|
],
|
2022-07-15 00:07:38 +00:00
|
|
|
MainConfigNames::NamespaceContentModels => [ 12312 => 'testing' ],
|
|
|
|
|
MainConfigNames::LanguageCode => $contLang->getCode(),
|
2020-10-23 03:17:31 +00:00
|
|
|
] );
|
|
|
|
|
$this->mergeMwGlobalArrayValue(
|
|
|
|
|
'wgContentHandlers',
|
|
|
|
|
[ 'testing' => 'DummyContentHandlerForTesting' ]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2020-10-28 02:43:05 +00:00
|
|
|
* Based on method in EditPageTest
|
|
|
|
|
* Performs an edit and checks the result matches the expected failure code
|
2020-10-23 03:17:31 +00:00
|
|
|
*
|
|
|
|
|
* @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.
|
2021-03-02 22:28:04 +00:00
|
|
|
* @param User|null $user The user to perform the edit as.
|
2020-10-23 03:17:31 +00:00
|
|
|
* @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
|
2020-10-28 02:43:05 +00:00
|
|
|
* @param int $expectedCode The expected result code (EditPage::AS_XXX constants).
|
|
|
|
|
* @param string $message Message to show along with any error message.
|
2020-10-23 03:17:31 +00:00
|
|
|
*
|
|
|
|
|
* @return WikiPage The page that was just edited, useful for getting the edit's rev_id, etc.
|
|
|
|
|
*/
|
2020-10-28 02:43:05 +00:00
|
|
|
protected function assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
$baseText,
|
2021-03-02 22:28:04 +00:00
|
|
|
?User $user,
|
2020-10-28 02:43:05 +00:00
|
|
|
array $edit,
|
|
|
|
|
$expectedCode,
|
|
|
|
|
$message
|
2020-10-23 03:17:31 +00:00
|
|
|
) {
|
|
|
|
|
if ( is_string( $title ) ) {
|
|
|
|
|
$ns = $this->getDefaultWikitextNS();
|
|
|
|
|
$title = Title::newFromText( $title, $ns );
|
|
|
|
|
}
|
|
|
|
|
$this->assertNotNull( $title );
|
|
|
|
|
|
2022-09-01 21:18:41 +00:00
|
|
|
$wikiPageFactory = $this->getServiceContainer()->getWikiPageFactory();
|
|
|
|
|
$page = $wikiPageFactory->newFromTitle( $title );
|
2021-06-24 08:42:19 +00:00
|
|
|
|
|
|
|
|
if ( $user == null ) {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-23 03:17:31 +00:00
|
|
|
if ( $baseText !== null ) {
|
|
|
|
|
$content = ContentHandler::makeContent( $baseText, $title );
|
2021-06-24 08:42:19 +00:00
|
|
|
$page->doUserEditContent( $content, $user, "base text for test" );
|
2020-10-23 03:17:31 +00:00
|
|
|
|
2020-10-28 02:43:05 +00:00
|
|
|
// Set the latest timestamp back a while
|
2021-04-29 02:37:11 +00:00
|
|
|
$dbw = wfGetDB( DB_PRIMARY );
|
2020-10-28 02:43:05 +00:00
|
|
|
$dbw->update(
|
|
|
|
|
'revision',
|
|
|
|
|
[ 'rev_timestamp' => $dbw->timestamp( '20120101000000' ) ],
|
|
|
|
|
[ 'rev_id' => $page->getLatest() ]
|
|
|
|
|
);
|
2020-10-23 03:17:31 +00:00
|
|
|
$page->clear();
|
2020-10-28 02:43:05 +00:00
|
|
|
|
2021-06-02 07:19:41 +00:00
|
|
|
$content = $page->getContent();
|
|
|
|
|
$this->assertInstanceOf( TextContent::class, $content );
|
|
|
|
|
$currentText = $content->getText();
|
2020-10-23 03:17:31 +00:00
|
|
|
|
|
|
|
|
# EditPage rtrim() the user input, so we alter our expected text
|
|
|
|
|
# to reflect that.
|
2020-10-28 02:43:05 +00:00
|
|
|
$this->assertEquals(
|
|
|
|
|
rtrim( $baseText ),
|
|
|
|
|
rtrim( $currentText ),
|
2021-11-21 19:13:24 +00:00
|
|
|
'page should have the text specified'
|
2020-10-28 02:43:05 +00:00
|
|
|
);
|
2020-10-23 03:17:31 +00:00
|
|
|
}
|
|
|
|
|
|
2021-08-05 06:54:11 +00:00
|
|
|
if ( !isset( $edit['wpEditToken'] ) ) {
|
|
|
|
|
$edit['wpEditToken'] = $user->getEditToken();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !isset( $edit['wpEdittime'] ) && !isset( $edit['editRevId'] ) ) {
|
|
|
|
|
$edit['wpEdittime'] = $page->exists() ? $page->getTimestamp() : '';
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-23 03:17:31 +00:00
|
|
|
if ( !isset( $edit['wpStarttime'] ) ) {
|
|
|
|
|
$edit['wpStarttime'] = wfTimestampNow();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( !isset( $edit['wpUnicodeCheck'] ) ) {
|
|
|
|
|
$edit['wpUnicodeCheck'] = EditPage::UNICODE_CHECK;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$req = new FauxRequest( $edit, true ); // session ??
|
2021-08-05 06:54:11 +00:00
|
|
|
|
2020-10-23 03:17:31 +00:00
|
|
|
$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 );
|
|
|
|
|
|
2023-03-24 01:14:02 +00:00
|
|
|
$bot = !empty( $edit['bot'] );
|
2020-10-23 03:17:31 +00:00
|
|
|
|
|
|
|
|
// this is where the edit happens!
|
2020-10-28 02:43:05 +00:00
|
|
|
// Note: don't want to use EditPage::attemptSave, because it messes with $wgOut
|
2020-10-23 03:17:31 +00:00
|
|
|
// and throws exceptions like PermissionsError
|
|
|
|
|
$status = $ep->internalAttemptSave( $result, $bot );
|
|
|
|
|
|
2020-10-28 02:43:05 +00:00
|
|
|
// check edit code
|
|
|
|
|
$this->assertSame(
|
|
|
|
|
$expectedCode,
|
|
|
|
|
$status->value,
|
|
|
|
|
"Expected result code mismatch. $message"
|
|
|
|
|
);
|
2020-10-23 03:17:31 +00:00
|
|
|
|
2022-09-01 21:18:41 +00:00
|
|
|
return $wikiPageFactory->newFromTitle( $title );
|
2020-10-23 03:17:31 +00:00
|
|
|
}
|
|
|
|
|
|
2020-11-12 05:10:17 +00:00
|
|
|
/** AccidentalRecreationConstraint integration */
|
|
|
|
|
public function testAccidentalRecreationConstraint() {
|
|
|
|
|
// Make sure it exists
|
|
|
|
|
$this->getExistingTestPage( 'AccidentalRecreationConstraintPage' );
|
|
|
|
|
|
|
|
|
|
// And now delete it, so that there is a deletion log
|
2022-12-04 19:09:28 +00:00
|
|
|
$page = $this->getNonexistingTestPage( 'AccidentalRecreationConstraintPage' );
|
2020-11-12 05:10:17 +00:00
|
|
|
$title = $page->getTitle();
|
|
|
|
|
|
|
|
|
|
// Set the time of the deletion to be a specific time, so we can be sure to start the
|
|
|
|
|
// edit before it. Since the constraint will query for the most recent timestamp,
|
|
|
|
|
// update *all* deletion logs for the page to the same timestamp (1 January 2020)
|
2021-04-29 02:37:11 +00:00
|
|
|
$dbw = wfGetDB( DB_PRIMARY );
|
2020-11-12 05:10:17 +00:00
|
|
|
$dbw->update(
|
|
|
|
|
'logging',
|
|
|
|
|
[ 'log_timestamp' => $dbw->timestamp( '20200101000000' ) ],
|
|
|
|
|
[
|
|
|
|
|
'log_namespace' => $title->getNamespace(),
|
2022-12-04 19:09:28 +00:00
|
|
|
'log_title' => $title->getDBkey(),
|
2020-11-12 05:10:17 +00:00
|
|
|
'log_type' => 'delete',
|
|
|
|
|
'log_action' => 'delete'
|
|
|
|
|
],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
|
|
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
|
|
|
|
// Needs edit rights to pass EditRightConstraint and reach AccidentalRecreationConstraint
|
|
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit' ] );
|
|
|
|
|
|
|
|
|
|
// Started the edit on 1 January 2019, page was deleted on 1 January 2020
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'New content',
|
|
|
|
|
'wpStarttime' => '20190101000000'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
null,
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_ARTICLE_WAS_DELETED,
|
|
|
|
|
'expected AS_ARTICLE_WAS_DELETED update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-11 00:28:56 +00:00
|
|
|
/** AutoSummaryMissingSummaryConstraint integration */
|
|
|
|
|
public function testAutoSummaryMissingSummaryConstraint() {
|
|
|
|
|
// Require the summary
|
|
|
|
|
$this->mergeMwGlobalArrayValue(
|
|
|
|
|
'wgDefaultUserOptions',
|
|
|
|
|
[ 'forceeditsummary' => 1 ]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$page = $this->getExistingTestPage( 'AutoSummaryMissingSummaryConstraint page does exist' );
|
|
|
|
|
$title = $page->getTitle();
|
|
|
|
|
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
|
|
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
EditPage: Disentangle edit summary and section title
Previously, `$this->summary` was used for two different purposes.
Usually it was just the summary. But when `$this->section` was 'new',
then it was actually the section title most of the time – unless
`$this->sectiontitle` was also set (in which case it took priority),
and until it was replaced by the real edit summary (near the end of
the processing, after copying the section title to the page content
and before saving changes).
Unsurprisingly some of the code didn't handle this duality correctly,
causing T191722 and T311533.
Now `$this->summary` is always the summary, and when `$this->section`
is 'new', then `$this->sectiontitle` is always the new section title.
The only place where this duality remains is in the input attributes
and query parameters, where 'wpSummary' is still used for both the
summary and the section title inputs (only one of them can appear,
depending on whether `$this->section` is 'new'). It would be an
unreasonable backwards-compatibility break to change this, and the
code handling this is somewhat isolated from the rest of the logic.
Bug: T191722
Bug: T311533
Change-Id: I5313ca9a045d112ece390b011a34192220e2abc1
2022-07-15 00:46:52 +00:00
|
|
|
// Needs edit rights to pass EditRightConstraint and reach NewSectionMissingSubjectConstraint
|
2020-11-11 00:28:56 +00:00
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit' ] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'New content, different from base content',
|
|
|
|
|
'wpSummary' => 'SameAsAutoSummary',
|
|
|
|
|
'wpAutoSummary' => md5( 'SameAsAutoSummary' )
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
'Base content, different from new content',
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_SUMMARY_NEEDED,
|
|
|
|
|
'expected AS_SUMMARY_NEEDED update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-27 18:36:46 +00:00
|
|
|
/** ChangeTagsConstraint integration */
|
|
|
|
|
public function testChangeTagsConstraint() {
|
|
|
|
|
// Remove rights
|
|
|
|
|
$this->mergeMwGlobalArrayValue(
|
|
|
|
|
'wgRevokePermissions',
|
|
|
|
|
[ 'user' => [ 'applychangetags' => true ] ]
|
|
|
|
|
);
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'Text',
|
|
|
|
|
'wpChangeTags' => 'tag-name'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
'EditPageTest_changeTagsConstraint',
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_CHANGE_TAG_ERROR,
|
|
|
|
|
'expected AS_CHANGE_TAG_ERROR update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-23 03:17:31 +00:00
|
|
|
/** ContentModelChangeConstraint integration */
|
|
|
|
|
public function testContentModelChangeConstraint() {
|
2021-03-02 22:28:04 +00:00
|
|
|
$user = $this->getTestUser()->getUser();
|
2020-10-23 03:17:31 +00:00
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
|
|
|
|
// Needs edit rights to pass EditRightConstraint and reach ContentModelChangeConstraint
|
|
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit' ] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'New text goes here',
|
|
|
|
|
'wpSummary' => 'Summary',
|
|
|
|
|
'model' => CONTENT_MODEL_TEXT,
|
|
|
|
|
'format' => CONTENT_FORMAT_TEXT,
|
|
|
|
|
];
|
|
|
|
|
|
2022-09-23 19:53:11 +00:00
|
|
|
$title = Title::makeTitle( NS_MAIN, 'Example' );
|
2020-10-23 03:17:31 +00:00
|
|
|
$this->assertSame(
|
|
|
|
|
CONTENT_MODEL_WIKITEXT,
|
|
|
|
|
$title->getContentModel(),
|
2021-11-21 19:13:24 +00:00
|
|
|
'title should start as wikitext content model'
|
2020-10-23 03:17:31 +00:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
'Base text',
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_NO_CHANGE_CONTENT_MODEL,
|
|
|
|
|
'expected AS_NO_CHANGE_CONTENT_MODEL update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-03 03:05:33 +00:00
|
|
|
/** CreationPermissionConstraint integration */
|
|
|
|
|
public function testCreationPermissionConstraint() {
|
|
|
|
|
$page = $this->getNonexistingTestPage( 'CreationPermissionConstraint page does not exist' );
|
|
|
|
|
$title = $page->getTitle();
|
|
|
|
|
|
2021-03-02 22:28:04 +00:00
|
|
|
$user = $this->getTestUser()->getUser();
|
2020-11-03 03:05:33 +00:00
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
|
|
|
|
// Needs edit rights to pass EditRightConstraint and reach CreationPermissionConstraint
|
|
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit' ] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'Page content',
|
|
|
|
|
'wpSummary' => 'Summary'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
null,
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_NO_CREATE_PERMISSION,
|
|
|
|
|
'expected AS_NO_CREATE_PERMISSION creation'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** DefaultTextConstraint integration */
|
|
|
|
|
public function testDefaultTextConstraint() {
|
|
|
|
|
$page = $this->getNonexistingTestPage( 'DefaultTextConstraint page does not exist' );
|
|
|
|
|
$title = $page->getTitle();
|
|
|
|
|
|
2021-03-02 22:28:04 +00:00
|
|
|
$user = $this->getTestUser()->getUser();
|
2020-11-03 03:05:33 +00:00
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
|
|
|
|
// Needs edit and createpage rights to pass EditRightConstraint and CreationPermissionConstraint
|
|
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit', 'createpage' ] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => '',
|
|
|
|
|
'wpSummary' => 'Summary'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
null,
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_BLANK_ARTICLE,
|
|
|
|
|
'expected AS_BLANK_ARTICLE creation'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-07 02:10:43 +00:00
|
|
|
/**
|
|
|
|
|
* EditFilterMergedContentHookConstraint integration
|
|
|
|
|
* @dataProvider provideTestEditFilterMergedContentHookConstraint
|
|
|
|
|
* @param bool $hookReturn
|
|
|
|
|
* @param ?int $statusValue
|
|
|
|
|
* @param bool $statusFatal
|
|
|
|
|
* @param int $expectedFailure
|
|
|
|
|
* @param string $expectedFailureStr
|
|
|
|
|
*/
|
|
|
|
|
public function testEditFilterMergedContentHookConstraint(
|
|
|
|
|
bool $hookReturn,
|
|
|
|
|
$statusValue,
|
|
|
|
|
bool $statusFatal,
|
|
|
|
|
int $expectedFailure,
|
|
|
|
|
string $expectedFailureStr
|
|
|
|
|
) {
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'EditFilterMergedContent',
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( $context, $content, $status, $summary, $user, $minorEdit )
|
2020-11-07 02:10:43 +00:00
|
|
|
use ( $hookReturn, $statusValue, $statusFatal )
|
|
|
|
|
{
|
|
|
|
|
if ( $statusValue !== null ) {
|
|
|
|
|
$status->value = $statusValue;
|
|
|
|
|
}
|
|
|
|
|
if ( $statusFatal ) {
|
|
|
|
|
$status->fatal( 'SomeErrorInTheHook' );
|
|
|
|
|
}
|
|
|
|
|
return $hookReturn;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
2021-03-02 22:28:04 +00:00
|
|
|
$user = $this->getTestUser()->getUser();
|
2020-11-07 02:10:43 +00:00
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
|
|
|
|
// Needs edit and createpage rights to pass EditRightConstraint and CreationPermissionConstraint
|
|
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit', 'createpage' ] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'Text',
|
|
|
|
|
'wpSummary' => 'Summary'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
'EditPageTest_testEditFilterMergedContentHookConstraint',
|
|
|
|
|
null,
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
$expectedFailure,
|
|
|
|
|
"expected $expectedFailureStr creation"
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideTestEditFilterMergedContentHookConstraint() {
|
2020-11-07 02:10:43 +00:00
|
|
|
yield 'Hook returns false, status is good, no value set' => [
|
2021-02-17 04:11:46 +00:00
|
|
|
false, null, false, EditPage::AS_HOOK_ERROR_EXPECTED, 'AS_HOOK_ERROR_EXPECTED'
|
2020-11-07 02:10:43 +00:00
|
|
|
];
|
|
|
|
|
yield 'Hook returns false, status is good, value set' => [
|
|
|
|
|
false, 1234567, false, 1234567, 'custom value 1234567'
|
|
|
|
|
];
|
|
|
|
|
yield 'Hook returns false, status is not good' => [
|
2021-02-17 04:11:46 +00:00
|
|
|
false, null, true, EditPage::AS_HOOK_ERROR_EXPECTED, 'AS_HOOK_ERROR_EXPECTED'
|
2020-11-07 02:10:43 +00:00
|
|
|
];
|
|
|
|
|
yield 'Hook returns true, status is not ok' => [
|
|
|
|
|
true, null, true, EditPage::AS_HOOK_ERROR_EXPECTED, 'AS_HOOK_ERROR_EXPECTED'
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-23 03:17:31 +00:00
|
|
|
/**
|
|
|
|
|
* EditRightConstraint integration
|
|
|
|
|
* @dataProvider provideTestEditRightConstraint
|
|
|
|
|
* @param bool $anon
|
|
|
|
|
* @param int $expectedErrorCode
|
|
|
|
|
*/
|
|
|
|
|
public function testEditRightConstraint( $anon, $expectedErrorCode ) {
|
2021-03-02 22:28:04 +00:00
|
|
|
if ( $anon ) {
|
|
|
|
|
$user = $this->getServiceContainer()->getUserFactory()->newAnonymous( '127.0.0.1' );
|
|
|
|
|
} else {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
}
|
2020-10-23 03:17:31 +00:00
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
|
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'Page content',
|
|
|
|
|
'wpSummary' => 'Summary'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
2020-10-28 02:43:05 +00:00
|
|
|
'EditPageTest_noEditRight',
|
|
|
|
|
'base text',
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
$expectedErrorCode,
|
|
|
|
|
'expected AS_READ_ONLY_PAGE_* update'
|
2020-10-23 03:17:31 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideTestEditRightConstraint() {
|
2020-10-23 03:17:31 +00:00
|
|
|
yield 'Anonymous user' => [ true, EditPage::AS_READ_ONLY_PAGE_ANON ];
|
|
|
|
|
yield 'Registered user' => [ false, EditPage::AS_READ_ONLY_PAGE_LOGGED ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ImageRedirectConstraint integration
|
|
|
|
|
* @dataProvider provideTestImageRedirectConstraint
|
|
|
|
|
* @param bool $anon
|
|
|
|
|
* @param int $expectedErrorCode
|
|
|
|
|
*/
|
|
|
|
|
public function testImageRedirectConstraint( $anon, $expectedErrorCode ) {
|
2021-03-02 22:28:04 +00:00
|
|
|
if ( $anon ) {
|
|
|
|
|
$user = $this->getServiceContainer()->getUserFactory()->newAnonymous( '127.0.0.1' );
|
|
|
|
|
} else {
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
}
|
2020-10-23 03:17:31 +00:00
|
|
|
|
|
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
|
|
|
|
// Needs edit rights to pass EditRightConstraint and reach ImageRedirectConstraint
|
|
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit' ] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => '#REDIRECT [[File:Example other file.jpg]]',
|
|
|
|
|
'wpSummary' => 'Summary'
|
|
|
|
|
];
|
|
|
|
|
|
2022-09-23 19:53:11 +00:00
|
|
|
$title = Title::makeTitle( NS_FILE, 'Example.jpg' );
|
2020-10-23 03:17:31 +00:00
|
|
|
$this->assertEdit(
|
2020-10-28 02:43:05 +00:00
|
|
|
$title,
|
|
|
|
|
null,
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
$expectedErrorCode,
|
|
|
|
|
'expected AS_IMAGE_REDIRECT_* update'
|
2020-10-23 03:17:31 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-03-23 11:36:19 +00:00
|
|
|
public static function provideTestImageRedirectConstraint() {
|
2020-10-23 03:17:31 +00:00
|
|
|
yield 'Anonymous user' => [ true, EditPage::AS_IMAGE_REDIRECT_ANON ];
|
|
|
|
|
yield 'Registered user' => [ false, EditPage::AS_IMAGE_REDIRECT_LOGGED ];
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-03 03:05:33 +00:00
|
|
|
/** MissingCommentConstraint integration */
|
|
|
|
|
public function testMissingCommentConstraint() {
|
|
|
|
|
$page = $this->getExistingTestPage( 'MissingCommentConstraint page does exist' );
|
|
|
|
|
$title = $page->getTitle();
|
|
|
|
|
|
2021-03-02 22:28:04 +00:00
|
|
|
$user = $this->getTestUser()->getUser();
|
2020-11-03 03:05:33 +00:00
|
|
|
|
|
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
|
|
|
|
// Needs edit rights to pass EditRightConstraint and reach MissingCommentConstraint
|
|
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit' ] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => '',
|
|
|
|
|
'wpSection' => 'new',
|
|
|
|
|
'wpSummary' => 'Summary'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
null,
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_TEXTBOX_EMPTY,
|
|
|
|
|
'expected AS_TEXTBOX_EMPTY update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
EditPage: Disentangle edit summary and section title
Previously, `$this->summary` was used for two different purposes.
Usually it was just the summary. But when `$this->section` was 'new',
then it was actually the section title most of the time – unless
`$this->sectiontitle` was also set (in which case it took priority),
and until it was replaced by the real edit summary (near the end of
the processing, after copying the section title to the page content
and before saving changes).
Unsurprisingly some of the code didn't handle this duality correctly,
causing T191722 and T311533.
Now `$this->summary` is always the summary, and when `$this->section`
is 'new', then `$this->sectiontitle` is always the new section title.
The only place where this duality remains is in the input attributes
and query parameters, where 'wpSummary' is still used for both the
summary and the section title inputs (only one of them can appear,
depending on whether `$this->section` is 'new'). It would be an
unreasonable backwards-compatibility break to change this, and the
code handling this is somewhat isolated from the rest of the logic.
Bug: T191722
Bug: T311533
Change-Id: I5313ca9a045d112ece390b011a34192220e2abc1
2022-07-15 00:46:52 +00:00
|
|
|
/** NewSectionMissingSubjectConstraint integration */
|
|
|
|
|
public function testNewSectionMissingSubjectConstraint() {
|
2020-11-03 03:05:33 +00:00
|
|
|
// Require the summary
|
|
|
|
|
$this->mergeMwGlobalArrayValue(
|
|
|
|
|
'wgDefaultUserOptions',
|
|
|
|
|
[ 'forceeditsummary' => 1 ]
|
|
|
|
|
);
|
|
|
|
|
|
EditPage: Disentangle edit summary and section title
Previously, `$this->summary` was used for two different purposes.
Usually it was just the summary. But when `$this->section` was 'new',
then it was actually the section title most of the time – unless
`$this->sectiontitle` was also set (in which case it took priority),
and until it was replaced by the real edit summary (near the end of
the processing, after copying the section title to the page content
and before saving changes).
Unsurprisingly some of the code didn't handle this duality correctly,
causing T191722 and T311533.
Now `$this->summary` is always the summary, and when `$this->section`
is 'new', then `$this->sectiontitle` is always the new section title.
The only place where this duality remains is in the input attributes
and query parameters, where 'wpSummary' is still used for both the
summary and the section title inputs (only one of them can appear,
depending on whether `$this->section` is 'new'). It would be an
unreasonable backwards-compatibility break to change this, and the
code handling this is somewhat isolated from the rest of the logic.
Bug: T191722
Bug: T311533
Change-Id: I5313ca9a045d112ece390b011a34192220e2abc1
2022-07-15 00:46:52 +00:00
|
|
|
$page = $this->getExistingTestPage( 'NewSectionMissingSubjectConstraint page does exist' );
|
2020-11-03 03:05:33 +00:00
|
|
|
$title = $page->getTitle();
|
|
|
|
|
|
|
|
|
|
$user = $this->getTestUser()->getUser();
|
|
|
|
|
|
|
|
|
|
$permissionManager = $this->getServiceContainer()->getPermissionManager();
|
EditPage: Disentangle edit summary and section title
Previously, `$this->summary` was used for two different purposes.
Usually it was just the summary. But when `$this->section` was 'new',
then it was actually the section title most of the time – unless
`$this->sectiontitle` was also set (in which case it took priority),
and until it was replaced by the real edit summary (near the end of
the processing, after copying the section title to the page content
and before saving changes).
Unsurprisingly some of the code didn't handle this duality correctly,
causing T191722 and T311533.
Now `$this->summary` is always the summary, and when `$this->section`
is 'new', then `$this->sectiontitle` is always the new section title.
The only place where this duality remains is in the input attributes
and query parameters, where 'wpSummary' is still used for both the
summary and the section title inputs (only one of them can appear,
depending on whether `$this->section` is 'new'). It would be an
unreasonable backwards-compatibility break to change this, and the
code handling this is somewhat isolated from the rest of the logic.
Bug: T191722
Bug: T311533
Change-Id: I5313ca9a045d112ece390b011a34192220e2abc1
2022-07-15 00:46:52 +00:00
|
|
|
// Needs edit rights to pass EditRightConstraint and reach NewSectionMissingSubjectConstraint
|
2020-11-03 03:05:33 +00:00
|
|
|
$permissionManager->overrideUserRightsForTesting( $user, [ 'edit' ] );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'Comment',
|
|
|
|
|
'wpSection' => 'new',
|
|
|
|
|
'wpSummary' => ''
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
null,
|
|
|
|
|
$user,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_SUMMARY_NEEDED,
|
|
|
|
|
'expected AS_SUMMARY_NEEDED update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-27 18:36:46 +00:00
|
|
|
/** PageSizeConstraint integration */
|
|
|
|
|
public function testPageSizeConstraintBeforeMerge() {
|
2019-09-09 08:49:23 +00:00
|
|
|
// Max size: 1 kibibyte
|
2022-07-15 00:07:38 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::MaxArticleSize, 1 );
|
2020-10-27 18:36:46 +00:00
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => str_repeat( 'text', 1000 )
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
'EditPageTest_pageSizeConstraintBeforeMerge',
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_CONTENT_TOO_BIG,
|
|
|
|
|
'expected AS_CONTENT_TOO_BIG update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** PageSizeConstraint integration */
|
|
|
|
|
public function testPageSizeConstraintAfterMerge() {
|
2019-09-09 08:49:23 +00:00
|
|
|
// Max size: 1 kibibyte
|
2022-07-15 00:07:38 +00:00
|
|
|
$this->overrideConfigValue( MainConfigNames::MaxArticleSize, 1 );
|
2020-10-27 18:36:46 +00:00
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpSection' => 'new',
|
|
|
|
|
'wpTextbox1' => str_repeat( 'b', 600 )
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
'EditPageTest_pageSizeConstraintAfterMerge',
|
|
|
|
|
str_repeat( 'a', 600 ),
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_MAX_ARTICLE_SIZE_EXCEEDED,
|
|
|
|
|
'expected AS_MAX_ARTICLE_SIZE_EXCEEDED update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-23 03:17:31 +00:00
|
|
|
/** ReadOnlyConstraint integration */
|
|
|
|
|
public function testReadOnlyConstraint() {
|
|
|
|
|
$readOnlyMode = $this->createMock( ReadOnlyMode::class );
|
|
|
|
|
$readOnlyMode->method( 'isReadOnly' )->willReturn( true );
|
|
|
|
|
$this->setService( 'ReadOnlyMode', $readOnlyMode );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'Text goes here'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
'EditPageTest_readOnlyConstraint',
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_READ_ONLY_PAGE,
|
|
|
|
|
'expected AS_READ_ONLY_PAGE update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-11 00:28:56 +00:00
|
|
|
/** SelfRedirectConstraint integration */
|
|
|
|
|
public function testSelfRedirectConstraint() {
|
|
|
|
|
// Use a page that does not exist to be sure that it is not already a self redirect
|
|
|
|
|
$page = $this->getNonexistingTestPage( 'SelfRedirectConstraint page does not exist' );
|
|
|
|
|
$title = $page->getTitle();
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => '#REDIRECT [[SelfRedirectConstraint page does not exist]]',
|
|
|
|
|
'wpSummary' => 'Redirect to self'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
$title,
|
|
|
|
|
'zero',
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_SELF_REDIRECT,
|
|
|
|
|
'expected AS_SELF_REDIRECT update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2020-10-23 03:17:31 +00:00
|
|
|
/** SimpleAntiSpamConstraint integration */
|
|
|
|
|
public function testSimpleAntiSpamConstraint() {
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'one',
|
|
|
|
|
'wpSummary' => 'first update',
|
|
|
|
|
'wpAntispam' => 'tatata'
|
|
|
|
|
];
|
2020-10-28 02:43:05 +00:00
|
|
|
$this->assertEdit(
|
|
|
|
|
'EditPageTest_testUpdatePageSpamError',
|
|
|
|
|
'zero',
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_SPAM_ERROR,
|
|
|
|
|
'expected AS_SPAM_ERROR update'
|
|
|
|
|
);
|
2020-10-23 03:17:31 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** SpamRegexConstraint integration */
|
|
|
|
|
public function testSpamRegexConstraint() {
|
|
|
|
|
$spamChecker = $this->createMock( SpamChecker::class );
|
|
|
|
|
$spamChecker->method( 'checkContent' )
|
2022-11-12 18:57:12 +00:00
|
|
|
->willReturnArgument( 0 );
|
2020-10-23 03:17:31 +00:00
|
|
|
$spamChecker->method( 'checkSummary' )
|
2022-11-12 18:57:12 +00:00
|
|
|
->willReturnArgument( 0 );
|
2020-10-23 03:17:31 +00:00
|
|
|
$this->setService( 'SpamChecker', $spamChecker );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'two',
|
|
|
|
|
'wpSummary' => 'spam summary'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
2020-10-28 02:43:05 +00:00
|
|
|
'EditPageTest_testUpdatePageSpamRegexError',
|
|
|
|
|
'zero',
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_SPAM_ERROR,
|
|
|
|
|
'expected AS_SPAM_ERROR update'
|
2020-10-23 03:17:31 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** UserBlockConstraint integration */
|
|
|
|
|
public function testUserBlockConstraint() {
|
|
|
|
|
$user = $this->createMock( User::class );
|
|
|
|
|
$user->method( 'getName' )->willReturn( 'NameGoesHere' );
|
|
|
|
|
$user->method( 'getId' )->willReturn( 12345 );
|
|
|
|
|
|
|
|
|
|
$permissionManager = $this->createMock( PermissionManager::class );
|
|
|
|
|
// Needs edit rights to pass EditRightConstraint and reach UserBlockConstraint
|
|
|
|
|
$permissionManager->method( 'userHasRight' )->willReturn( true );
|
2021-06-24 08:42:19 +00:00
|
|
|
$permissionManager->method( 'userCan' )->willReturn( true );
|
2020-10-23 03:17:31 +00:00
|
|
|
|
|
|
|
|
// Not worried about the specifics of the method call, those are tested in
|
|
|
|
|
// the UserBlockConstraintTest
|
|
|
|
|
$permissionManager->method( 'isBlockedFrom' )->willReturn( true );
|
|
|
|
|
|
|
|
|
|
$this->setService( 'PermissionManager', $permissionManager );
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'Page content',
|
|
|
|
|
'wpSummary' => 'Summary'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
'EditPageTest_userBlocked',
|
|
|
|
|
'base text',
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_BLOCKED_PAGE_FOR_USER,
|
|
|
|
|
'expected AS_BLOCKED_PAGE_FOR_USER update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** UserRateLimitConstraint integration */
|
|
|
|
|
public function testUserRateLimitConstraint() {
|
|
|
|
|
$this->setTemporaryHook(
|
|
|
|
|
'PingLimiter',
|
2021-02-07 13:10:36 +00:00
|
|
|
static function ( $user, $action, &$result, $incrBy ) {
|
2020-10-23 03:17:31 +00:00
|
|
|
// Always fail
|
|
|
|
|
$result = true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$edit = [
|
|
|
|
|
'wpTextbox1' => 'Text goes here'
|
|
|
|
|
];
|
|
|
|
|
$this->assertEdit(
|
|
|
|
|
'EditPageTest_userRateLimitConstraint',
|
|
|
|
|
null,
|
|
|
|
|
null,
|
|
|
|
|
$edit,
|
|
|
|
|
EditPage::AS_RATE_LIMITED,
|
|
|
|
|
'expected AS_RATE_LIMITED update'
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|