From 0813c46daaa7ca1b52008633a5eb8a769df67137 Mon Sep 17 00:00:00 2001 From: David Barratt Date: Mon, 27 Aug 2018 18:19:37 -0400 Subject: [PATCH] Update Special:Block to set Partial Blocks Make the necessary UI changes to Special:Block in order to set/update partial blocks. Bug: T197109 Change-Id: Ib3067824b5dcbdd893ab1f165d169a35d0716cb2 --- includes/api/ApiBlock.php | 42 +- includes/api/i18n/en.json | 2 + includes/api/i18n/qqq.json | 2 + includes/specials/SpecialBlock.php | 153 +++++-- languages/i18n/en.json | 4 + languages/i18n/qqq.json | 4 + resources/Resources.php | 2 + resources/src/mediawiki.special.block.js | 11 +- resources/src/mediawiki.special.block.less | 6 + tests/phpunit/includes/api/ApiBlockTest.php | 66 +++ .../includes/specials/SpecialBlockTest.php | 405 ++++++++++++++++++ 11 files changed, 659 insertions(+), 38 deletions(-) create mode 100644 resources/src/mediawiki.special.block.less create mode 100644 tests/phpunit/includes/specials/SpecialBlockTest.php diff --git a/includes/api/ApiBlock.php b/includes/api/ApiBlock.php index 8f4028310a..3581ac8514 100644 --- a/includes/api/ApiBlock.php +++ b/includes/api/ApiBlock.php @@ -54,6 +54,30 @@ class ApiBlock extends ApiBase { } } + $editingRestriction = 'sitewide'; + $pageRestrictions = ''; + if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) { + if ( $params['pagerestrictions'] ) { + $count = count( $params['pagerestrictions'] ); + if ( $count > 10 ) { + $this->dieWithError( + $this->msg( + 'apierror-integeroutofrange-abovebotmax', + 'pagerestrictions', + 10, + $count + ) + ); + } + } + + if ( $params['partial'] ) { + $editingRestriction = 'partial'; + } + + $pageRestrictions = implode( "\n", $params['pagerestrictions'] ); + } + if ( $params['userid'] !== null ) { $username = User::whoIs( $params['userid'] ); @@ -107,6 +131,8 @@ class ApiBlock extends ApiBase { 'Watch' => $params['watchuser'], 'Confirm' => true, 'Tags' => $params['tags'], + 'EditingRestriction' => $editingRestriction, + 'PageRestrictions' => $pageRestrictions, ]; $retval = SpecialBlock::processForm( $data, $this->getContext() ); @@ -137,6 +163,11 @@ class ApiBlock extends ApiBase { $res['allowusertalk'] = $params['allowusertalk']; $res['watchuser'] = $params['watchuser']; + if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) { + $res['partial'] = $params['partial']; + $res['pagerestrictions'] = $params['pagerestrictions']; + } + $this->getResult()->addValue( null, $this->getModuleName(), $res ); } @@ -149,7 +180,7 @@ class ApiBlock extends ApiBase { } public function getAllowedParams() { - return [ + $params = [ 'user' => [ ApiBase::PARAM_TYPE => 'user', ], @@ -171,6 +202,15 @@ class ApiBlock extends ApiBase { ApiBase::PARAM_ISMULTI => true, ], ]; + + if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) { + $params['partial'] = false; + $params['pagerestrictions'] = [ + ApiBase::PARAM_ISMULTI => true, + ]; + } + + return $params; } public function needsToken() { diff --git a/includes/api/i18n/en.json b/includes/api/i18n/en.json index 25bf3f74a7..fb5226addf 100644 --- a/includes/api/i18n/en.json +++ b/includes/api/i18n/en.json @@ -40,6 +40,8 @@ "apihelp-block-param-reblock": "If the user is already blocked, overwrite the existing block.", "apihelp-block-param-watchuser": "Watch the user's or IP address's user and talk pages.", "apihelp-block-param-tags": "Change tags to apply to the entry in the block log.", + "apihelp-block-param-partial": "Block user from specific pages or namespaces rather than the entire site.", + "apihelp-block-param-pagerestrictions": "List of titles to block the user from editing. Only applies when 'partial' is set to true.", "apihelp-block-example-ip-simple": "Block IP address 192.0.2.5 for three days with reason First strike.", "apihelp-block-example-user-complex": "Block user Vandal indefinitely with reason Vandalism, and prevent new account creation and email sending.", diff --git a/includes/api/i18n/qqq.json b/includes/api/i18n/qqq.json index d27933051a..d5dd47495a 100644 --- a/includes/api/i18n/qqq.json +++ b/includes/api/i18n/qqq.json @@ -48,6 +48,8 @@ "apihelp-block-param-reblock": "{{doc-apihelp-param|block|reblock}}", "apihelp-block-param-watchuser": "{{doc-apihelp-param|block|watchuser}}", "apihelp-block-param-tags": "{{doc-apihelp-param|block|tags}}", + "apihelp-block-param-partial": "{{doc-apihelp-param|block|partial}}", + "apihelp-block-param-pagerestrictions": "{{doc-apihelp-param|block|pagerestrictions}}", "apihelp-block-example-ip-simple": "{{doc-apihelp-example|block}}", "apihelp-block-example-user-complex": "{{doc-apihelp-example|block}}", "apihelp-changeauthenticationdata-summary": "{{doc-apihelp-summary|changeauthenticationdata}}", diff --git a/includes/specials/SpecialBlock.php b/includes/specials/SpecialBlock.php index 4708d2ba9e..4c50f6de52 100644 --- a/includes/specials/SpecialBlock.php +++ b/includes/specials/SpecialBlock.php @@ -21,6 +21,9 @@ * @ingroup SpecialPage */ +use MediaWiki\Block\BlockRestriction; +use MediaWiki\Block\Restriction\PageRestriction; + /** * A special page that allows users with 'block' right to block users from * editing pages and other actions @@ -137,41 +140,63 @@ class SpecialBlock extends FormSpecialPage { $conf = $this->getConfig(); $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD; + $enablePartialBlocks = $conf->get( 'EnablePartialBlocks' ); - $a = [ - 'Target' => [ - 'type' => 'user', - 'ipallowed' => true, - 'iprange' => true, - 'label-message' => 'ipaddressorusername', - 'id' => 'mw-bi-target', - 'size' => '45', - 'autofocus' => true, - 'required' => true, - 'validation-callback' => [ __CLASS__, 'validateTargetField' ], - ], - 'Expiry' => [ - 'type' => 'expiry', - 'label-message' => 'ipbexpiry', - 'required' => true, - 'options' => $suggestedDurations, - 'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(), - ], - 'Reason' => [ - 'type' => 'selectandother', - // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP - // (e.g. emojis) count for two each. This limit is overridden in JS to instead count - // Unicode codepoints (or 255 UTF-8 bytes for old schema). - 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT, - 'maxlength-unit' => 'codepoints', - 'label-message' => 'ipbreason', - 'options-message' => 'ipbreason-dropdown', - ], - 'CreateAccount' => [ - 'type' => 'check', - 'label-message' => 'ipbcreateaccount', - 'default' => true, - ], + $a = []; + + $a['Target'] = [ + 'type' => 'user', + 'ipallowed' => true, + 'iprange' => true, + 'label-message' => 'ipaddressorusername', + 'id' => 'mw-bi-target', + 'size' => '45', + 'autofocus' => true, + 'required' => true, + 'validation-callback' => [ __CLASS__, 'validateTargetField' ], + ]; + + if ( $enablePartialBlocks ) { + $a['EditingRestriction'] = [ + 'type' => 'radio', + 'label' => $this->msg( 'ipb-type-label' )->text(), + 'options' => [ + $this->msg( 'ipb-sitewide' )->text() => 'sitewide', + $this->msg( 'ipb-partial' )->text() => 'partial', + ], + ]; + $a['PageRestrictions'] = [ + 'type' => 'titlesmultiselect', + 'label' => $this->msg( 'ipb-pages-label' )->text(), + 'exists' => true, + 'max' => 10, + 'cssclass' => 'mw-block-page-restrictions', + ]; + } + + $a['Expiry'] = [ + 'type' => 'expiry', + 'label-message' => 'ipbexpiry', + 'required' => true, + 'options' => $suggestedDurations, + 'default' => $this->msg( 'ipb-default-expiry' )->inContentLanguage()->text(), + ]; + + $a['Reason'] = [ + 'type' => 'selectandother', + // HTML maxlength uses "UTF-16 code units", which means that characters outside BMP + // (e.g. emojis) count for two each. This limit is overridden in JS to instead count + // Unicode codepoints (or 255 UTF-8 bytes for old schema). + 'maxlength' => $oldCommentSchema ? 255 : CommentStore::COMMENT_CHARACTER_LIMIT, + 'maxlength-unit' => 'codepoints', + 'label-message' => 'ipbreason', + 'options-message' => 'ipbreason-dropdown', + ]; + + $a['CreateAccount'] = [ + 'type' => 'check', + 'label-message' => 'ipbcreateaccount', + 'default' => true, ]; if ( self::canBlockEmail( $user ) ) { @@ -327,6 +352,29 @@ class SpecialBlock extends FormSpecialPage { unset( $fields['Confirm']['default'] ); $this->preErrors[] = [ 'ipb-blockingself', 'ipb-confirmaction' ]; } + + if ( $this->getConfig()->get( 'EnablePartialBlocks' ) ) { + if ( $block instanceof Block && !$block->isSitewide() ) { + $fields['EditingRestriction']['default'] = 'partial'; + } else { + $fields['EditingRestriction']['default'] = 'sitewide'; + } + + if ( $block instanceof Block ) { + $pageRestrictions = []; + foreach ( $block->getRestrictions() as $restriction ) { + if ( $restriction->getType() !== 'page' ) { + continue; + } + + $pageRestrictions[] = $restriction->getTitle()->getPrefixedText(); + } + + // Sort the restrictions so they are in alphabetical order. + sort( $pageRestrictions ); + $fields['PageRestrictions']['default'] = implode( "\n", $pageRestrictions ); + } + } } /** @@ -632,6 +680,7 @@ class SpecialBlock extends FormSpecialPage { global $wgBlockAllowsUTEdit, $wgHideUserContribLimit; $performer = $context->getUser(); + $enablePartialBlocks = $context->getConfig()->get( 'EnablePartialBlocks' ); // Handled by field validator callback // self::validateTargetField( $data['Target'] ); @@ -740,11 +789,35 @@ class SpecialBlock extends FormSpecialPage { $block->isAutoblocking( $data['AutoBlock'] ); $block->mHideName = $data['HideUser']; + if ( + $enablePartialBlocks && + isset( $data['EditingRestriction'] ) && + $data['EditingRestriction'] === 'partial' + ) { + $block->isSitewide( false ); + } + $reason = [ 'hookaborted' ]; if ( !Hooks::run( 'BlockIp', [ &$block, &$performer, &$reason ] ) ) { return $reason; } + $restrictions = []; + if ( $enablePartialBlocks ) { + if ( !empty( $data['PageRestrictions'] ) ) { + $restrictions = array_map( function ( $text ) { + $title = Title::newFromText( $text ); + // Use the link cache since the title has already been loaded when + // the field was validated. + $restriction = new PageRestriction( 0, $title->getArticleId() ); + $restriction->setTitle( $title ); + return $restriction; + }, explode( "\n", $data['PageRestrictions'] ) ); + } + + $block->setRestrictions( $restrictions ); + } + $priorBlock = null; # Try to insert block. Is there a conflicting block? $status = $block->insert(); @@ -783,7 +856,17 @@ class SpecialBlock extends FormSpecialPage { $currentBlock->prevents( 'sendemail', $block->prevents( 'sendemail' ) ); $currentBlock->prevents( 'editownusertalk', $block->prevents( 'editownusertalk' ) ); $currentBlock->mReason = $block->mReason; - $currentBlock->isSitewide( $block->isSitewide() ); + + if ( $enablePartialBlocks ) { + // Maintain the sitewide status. If partial blocks is not enabled, + // saving the block will result in a sitewide block. + $currentBlock->isSitewide( $block->isSitewide() ); + + // Set the block id of the restrictions. + $currentBlock->setRestrictions( + BlockRestriction::setBlockId( $currentBlock->getId(), $restrictions ) + ); + } $status = $currentBlock->update(); diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 0580b0c8ee..ab0c439e5d 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -2591,6 +2591,10 @@ "ipb-disableusertalk": "Prevent this user from editing their own talk page while blocked", "ipb-change-block": "Re-block the user with these settings", "ipb-confirm": "Confirm block", + "ipb-sitewide": "Sitewide", + "ipb-partial": "Partial", + "ipb-type-label": "Type", + "ipb-pages-label": "Pages", "badipaddress": "Invalid IP address", "blockipsuccesssub": "Block succeeded", "blockipsuccesstext": "[[Special:Contributions/$1|$1]] has been blocked.
\nSee the [[Special:BlockList|block list]] to review blocks.", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 8dae3da6e4..82b7038f49 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -2793,6 +2793,10 @@ "ipb-disableusertalk": "{{doc-singularthey}}\nUsed as label for checkbox in [[Special:Block]].\n\nSee also:\n* {{msg-mw|ipbemailban}}\n* {{msg-mw|ipbenableautoblock}}\n* {{msg-mw|ipbhidename}}\n* {{msg-mw|ipbwatchuser}}\n* {{msg-mw|ipb-hardblock}}", "ipb-change-block": "Confirmation checkbox required for blocks that would override an earlier block. Appears together with {{msg-mw|ipb-needreblock}}.", "ipb-confirm": "Used as hidden field in the form on [[Special:Block]].", + "ipb-sitewide": "A type of block the user can select from on [[Special:Block]].", + "ipb-partial": "A type of block the user can select from on [[Special:Block]].", + "ipb-type-label": "The label of the type of editing restriction the admin would like to impose on [[Special:Block]].", + "ipb-pages-label": "The label for a autocomplete text field to specify pages to block a user from editing on [[Special:Block]].", "badipaddress": "An error message shown when one entered an invalid IP address in blocking page.", "blockipsuccesssub": "Used as page title in [[Special:Block]].\n\nThis message is the subject for the following message:\n* {{msg-mw|Blockipsuccesstext}}", "blockipsuccesstext": "Used in [[Special:Block]].\nThe title (subject) for this message is {{msg-mw|Blockipsuccesssub}}.\n\nParameters:\n* $1 - username, can be used for GENDER", diff --git a/resources/Resources.php b/resources/Resources.php index 49a3a845f5..e8466b72b8 100644 --- a/resources/Resources.php +++ b/resources/Resources.php @@ -2067,6 +2067,7 @@ return [ ], 'mediawiki.special.block' => [ 'scripts' => 'resources/src/mediawiki.special.block.js', + 'styles' => 'resources/src/mediawiki.special.block.less', 'dependencies' => [ 'oojs-ui-core', 'oojs-ui.styles.icons-editing-core', @@ -2077,6 +2078,7 @@ return [ 'mediawiki.htmlform', 'moment', ], + 'targets' => [ 'desktop', 'mobile' ], ], 'mediawiki.special.changecredentials.js' => [ 'scripts' => 'resources/src/mediawiki.special.changecredentials.js', diff --git a/resources/src/mediawiki.special.block.js b/resources/src/mediawiki.special.block.js index cb54e71d08..1852231e50 100644 --- a/resources/src/mediawiki.special.block.js +++ b/resources/src/mediawiki.special.block.js @@ -19,7 +19,9 @@ enableAutoblockField = infuseOrNull( $( '#mw-input-wpAutoBlock' ).closest( '.oo-ui-fieldLayout' ) ), hideUserField = infuseOrNull( $( '#mw-input-wpHideUser' ).closest( '.oo-ui-fieldLayout' ) ), watchUserField = infuseOrNull( $( '#mw-input-wpWatch' ).closest( '.oo-ui-fieldLayout' ) ), - expiryWidget = infuseOrNull( 'mw-input-wpExpiry' ); + expiryWidget = infuseOrNull( 'mw-input-wpExpiry' ), + editingRestrictionWidget = infuseOrNull( 'mw-input-wpEditingRestriction' ), + pageRestrictionsWidget = infuseOrNull( 'mw-input-wpPageRestrictions' ); function updateBlockOptions() { var blocktarget = blockTargetWidget.getValue().trim(), @@ -30,7 +32,8 @@ expiryValue = expiryWidget.getValue(), // infinityValues are the values the SpecialBlock class accepts as infinity (sf. wfIsInfinity) infinityValues = [ 'infinite', 'indefinite', 'infinity', 'never' ], - isIndefinite = infinityValues.indexOf( expiryValue ) !== -1; + isIndefinite = infinityValues.indexOf( expiryValue ) !== -1, + editingRestrictionValue = editingRestrictionWidget ? editingRestrictionWidget.getValue() : undefined; if ( enableAutoblockField ) { enableAutoblockField.toggle( !( isNonEmptyIp ) ); @@ -44,12 +47,16 @@ if ( watchUserField ) { watchUserField.toggle( !( isIpRange && !isEmpty ) ); } + if ( pageRestrictionsWidget ) { + pageRestrictionsWidget.setDisabled( editingRestrictionValue === 'sitewide' ); + } } if ( blockTargetWidget ) { // Bind functions so they're checked whenever stuff changes blockTargetWidget.on( 'change', updateBlockOptions ); expiryWidget.on( 'change', updateBlockOptions ); + editingRestrictionWidget.on( 'change', updateBlockOptions ); // Call them now to set initial state (ie. Special:Block/Foobar?wpBlockExpiry=2+hours) updateBlockOptions(); diff --git a/resources/src/mediawiki.special.block.less b/resources/src/mediawiki.special.block.less new file mode 100644 index 0000000000..c013994756 --- /dev/null +++ b/resources/src/mediawiki.special.block.less @@ -0,0 +1,6 @@ +.mw-block-page-restrictions { + margin-left: 2em; + .oo-ui-widget { + max-width: 48em; + } +} diff --git a/tests/phpunit/includes/api/ApiBlockTest.php b/tests/phpunit/includes/api/ApiBlockTest.php index 07e861feac..563d5e34d8 100644 --- a/tests/phpunit/includes/api/ApiBlockTest.php +++ b/tests/phpunit/includes/api/ApiBlockTest.php @@ -233,6 +233,26 @@ class ApiBlockTest extends ApiTestCase { $this->doBlock( [ 'expiry' => '' ] ); } + public function testBlockWithRestrictions() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => true, + ] ); + + $title = 'Foo'; + $page = $this->getExistingTestPage( $title ); + + $this->doBlock( [ + 'partial' => true, + 'pagerestrictions' => $title, + ] ); + + $block = Block::newFromTarget( $this->mUser->getName() ); + + $this->assertFalse( $block->isSitewide() ); + $this->assertCount( 1, $block->getRestrictions() ); + $this->assertEquals( $title, $block->getRestrictions()[0]->getTitle()->getText() ); + } + /** * @expectedException ApiUsageException * @expectedExceptionMessage The "token" parameter must be set @@ -249,4 +269,50 @@ class ApiBlockTest extends ApiTestCase { self::$users['sysop']->getUser() ); } + + /** + * @expectedException ApiUsageException + * @expectedExceptionMessage Invalid value "127.0.0.1/64" for user parameter "user". + */ + public function testBlockWithLargeRange() { + $tokens = $this->getTokens(); + + $this->doApiRequest( + [ + 'action' => 'block', + 'user' => '127.0.0.1/64', + 'reason' => 'Some reason', + 'token' => $tokens['blocktoken'], + ], + null, + false, + self::$users['sysop']->getUser() + ); + } + + /** + * @expectedException ApiUsageException + * @expectedExceptionMessage "pagerestrictions" may not be over 10 (set to 11) for bots or sysops. + */ + public function testBlockingToManyRestrictions() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => true, + ] ); + + $tokens = $this->getTokens(); + + $this->doApiRequest( + [ + 'action' => 'block', + 'user' => $this->mUser->getName(), + 'reason' => 'Some reason', + 'partial' => true, + 'pagerestrictions' => 'One|Two|Three|Four|Five|Six|Seven|Eight|Nine|Ten|Eleven', + 'token' => $tokens['blocktoken'], + ], + null, + false, + self::$users['sysop']->getUser() + ); + } } diff --git a/tests/phpunit/includes/specials/SpecialBlockTest.php b/tests/phpunit/includes/specials/SpecialBlockTest.php new file mode 100644 index 0000000000..080c6e4c43 --- /dev/null +++ b/tests/phpunit/includes/specials/SpecialBlockTest.php @@ -0,0 +1,405 @@ +resetTables(); + } + + /** + * @covers ::getFormFields() + */ + public function testGetFormFields() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => false, + ] ); + $page = $this->newSpecialPage(); + $wrappedPage = TestingAccessWrapper::newFromObject( $page ); + $fields = $wrappedPage->getFormFields(); + $this->assertInternalType( 'array', $fields ); + $this->assertArrayHasKey( 'Target', $fields ); + $this->assertArrayHasKey( 'Expiry', $fields ); + $this->assertArrayHasKey( 'Reason', $fields ); + $this->assertArrayHasKey( 'CreateAccount', $fields ); + $this->assertArrayHasKey( 'DisableUTEdit', $fields ); + $this->assertArrayHasKey( 'DisableUTEdit', $fields ); + $this->assertArrayHasKey( 'AutoBlock', $fields ); + $this->assertArrayHasKey( 'HardBlock', $fields ); + $this->assertArrayHasKey( 'PreviousTarget', $fields ); + $this->assertArrayHasKey( 'Confirm', $fields ); + + $this->assertArrayNotHasKey( 'EditingRestriction', $fields ); + $this->assertArrayNotHasKey( 'PageRestrictions', $fields ); + } + + /** + * @covers ::getFormFields() + */ + public function testGetFormFieldsPartialBlocks() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => true, + ] ); + $page = $this->newSpecialPage(); + $wrappedPage = TestingAccessWrapper::newFromObject( $page ); + $fields = $wrappedPage->getFormFields(); + + $this->assertArrayHasKey( 'EditingRestriction', $fields ); + $this->assertArrayHasKey( 'PageRestrictions', $fields ); + } + + /** + * @covers ::maybeAlterFormDefaults() + */ + public function testMaybeAlterFormDefaults() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => false, + ] ); + + $block = $this->insertBlock(); + + // Refresh the block from the database. + $block = Block::newFromTarget( $block->getTarget() ); + + $page = $this->newSpecialPage(); + + $wrappedPage = TestingAccessWrapper::newFromObject( $page ); + $wrappedPage->target = $block->getTarget(); + $fields = $wrappedPage->getFormFields(); + + $this->assertSame( (string)$block->getTarget(), $fields['Target']['default'] ); + $this->assertSame( $block->isHardblock(), $fields['HardBlock']['default'] ); + $this->assertSame( $block->prevents( 'createaccount' ), $fields['CreateAccount']['default'] ); + $this->assertSame( $block->isAutoblocking(), $fields['AutoBlock']['default'] ); + $this->assertSame( $block->prevents( 'editownusertalk' ), $fields['DisableUTEdit']['default'] ); + $this->assertSame( $block->mReason, $fields['Reason']['default'] ); + $this->assertSame( 'infinite', $fields['Expiry']['default'] ); + } + + /** + * @covers ::maybeAlterFormDefaults() + */ + public function testMaybeAlterFormDefaultsPartial() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => true, + ] ); + + $badActor = $this->getTestUser()->getUser(); + $sysop = $this->getTestSysop()->getUser(); + $pageSaturn = $this->getExistingTestPage( 'Saturn' ); + $pageMars = $this->getExistingTestPage( 'Mars' ); + + $block = new \Block( [ + 'address' => $badActor->getName(), + 'user' => $badActor->getId(), + 'by' => $sysop->getId(), + 'expiry' => 'infinity', + 'sitewide' => 0, + 'enableAutoblock' => true, + ] ); + + $block->setRestrictions( [ + new PageRestriction( 0, $pageSaturn->getId() ), + new PageRestriction( 0, $pageMars->getId() ), + ] ); + + $block->insert(); + + // Refresh the block from the database. + $block = Block::newFromTarget( $block->getTarget() ); + + $page = $this->newSpecialPage(); + + $wrappedPage = TestingAccessWrapper::newFromObject( $page ); + $wrappedPage->target = $block->getTarget(); + $fields = $wrappedPage->getFormFields(); + + $titles = [ + $pageMars->getTitle()->getPrefixedText(), + $pageSaturn->getTitle()->getPrefixedText(), + ]; + + $this->assertSame( (string)$block->getTarget(), $fields['Target']['default'] ); + $this->assertSame( 'partial', $fields['EditingRestriction']['default'] ); + $this->assertSame( implode( "\n", $titles ), $fields['PageRestrictions']['default'] ); + } + + /** + * @covers ::processForm() + */ + public function testProcessForm() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => false, + ] ); + $badActor = $this->getTestUser()->getUser(); + $context = RequestContext::getMain(); + + $page = $this->newSpecialPage(); + $reason = 'test'; + $expiry = 'infinity'; + $data = [ + 'Target' => (string)$badActor, + 'Expiry' => 'infinity', + 'Reason' => [ + $reason, + ], + 'Confirm' => '1', + 'CreateAccount' => '0', + 'DisableUTEdit' => '0', + 'DisableEmail' => '0', + 'HardBlock' => '0', + 'AutoBlock' => '1', + 'HideUser' => '0', + 'Watch' => '0', + ]; + $result = $page->processForm( $data, $context ); + + $this->assertTrue( $result ); + + $block = Block::newFromTarget( $badActor ); + $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $expiry, $block->getExpiry() ); + } + + /** + * @covers ::processForm() + */ + public function testProcessFormExisting() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => false, + ] ); + $badActor = $this->getTestUser()->getUser(); + $sysop = $this->getTestSysop()->getUser(); + $context = RequestContext::getMain(); + + // Create a block that will be updated. + $block = new \Block( [ + 'address' => $badActor->getName(), + 'user' => $badActor->getId(), + 'by' => $sysop->getId(), + 'expiry' => 'infinity', + 'sitewide' => 0, + 'enableAutoblock' => false, + ] ); + $block->insert(); + + $page = $this->newSpecialPage(); + $reason = 'test'; + $expiry = 'infinity'; + $data = [ + 'Target' => (string)$badActor, + 'Expiry' => 'infinity', + 'Reason' => [ + $reason, + ], + 'Confirm' => '1', + 'CreateAccount' => '0', + 'DisableUTEdit' => '0', + 'DisableEmail' => '0', + 'HardBlock' => '0', + 'AutoBlock' => '1', + 'HideUser' => '0', + 'Watch' => '0', + ]; + $result = $page->processForm( $data, $context ); + + $this->assertTrue( $result ); + + $block = Block::newFromTarget( $badActor ); + $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $expiry, $block->getExpiry() ); + $this->assertSame( '1', $block->isAutoblocking() ); + } + + /** + * @covers ::processForm() + */ + public function testProcessFormRestictions() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => true, + ] ); + $badActor = $this->getTestUser()->getUser(); + $context = RequestContext::getMain(); + + $pageSaturn = $this->getExistingTestPage( 'Saturn' ); + $pageMars = $this->getExistingTestPage( 'Mars' ); + + $titles = [ + $pageSaturn->getTitle()->getText(), + $pageMars->getTitle()->getText(), + ]; + + $page = $this->newSpecialPage(); + $reason = 'test'; + $expiry = 'infinity'; + $data = [ + 'Target' => (string)$badActor, + 'Expiry' => 'infinity', + 'Reason' => [ + $reason, + ], + 'Confirm' => '1', + 'CreateAccount' => '0', + 'DisableUTEdit' => '0', + 'DisableEmail' => '0', + 'HardBlock' => '0', + 'AutoBlock' => '1', + 'HideUser' => '0', + 'Watch' => '0', + 'EditingRestriction' => 'partial', + 'PageRestrictions' => implode( "\n", $titles ), + ]; + $result = $page->processForm( $data, $context ); + + $this->assertTrue( $result ); + + $block = Block::newFromTarget( $badActor ); + $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $expiry, $block->getExpiry() ); + $this->assertCount( 2, $block->getRestrictions() ); + $this->assertTrue( BlockRestriction::equals( $block->getRestrictions(), [ + new PageRestriction( $block->getId(), $pageMars->getId() ), + new PageRestriction( $block->getId(), $pageSaturn->getId() ), + ] ) ); + } + + /** + * @covers ::processForm() + */ + public function testProcessFormRestrictionsChange() { + $this->setMwGlobals( [ + 'wgEnablePartialBlocks' => true, + ] ); + $badActor = $this->getTestUser()->getUser(); + $context = RequestContext::getMain(); + + $pageSaturn = $this->getExistingTestPage( 'Saturn' ); + $pageMars = $this->getExistingTestPage( 'Mars' ); + + $titles = [ + $pageSaturn->getTitle()->getText(), + $pageMars->getTitle()->getText(), + ]; + + // Create a partial block. + $page = $this->newSpecialPage(); + $reason = 'test'; + $expiry = 'infinity'; + $data = [ + 'Target' => (string)$badActor, + 'Expiry' => 'infinity', + 'Reason' => [ + $reason, + ], + 'Confirm' => '1', + 'CreateAccount' => '0', + 'DisableUTEdit' => '0', + 'DisableEmail' => '0', + 'HardBlock' => '0', + 'AutoBlock' => '1', + 'HideUser' => '0', + 'Watch' => '0', + 'EditingRestriction' => 'partial', + 'PageRestrictions' => implode( "\n", $titles ), + ]; + $result = $page->processForm( $data, $context ); + + $this->assertTrue( $result ); + + $block = Block::newFromTarget( $badActor ); + $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $expiry, $block->getExpiry() ); + $this->assertFalse( $block->isSitewide() ); + $this->assertCount( 2, $block->getRestrictions() ); + $this->assertTrue( BlockRestriction::equals( $block->getRestrictions(), [ + new PageRestriction( $block->getId(), $pageMars->getId() ), + new PageRestriction( $block->getId(), $pageSaturn->getId() ), + ] ) ); + + // Remove a page from the partial block. + $data['PageRestrictions'] = $pageMars->getTitle()->getText(); + $result = $page->processForm( $data, $context ); + + $this->assertTrue( $result ); + + $block = Block::newFromTarget( $badActor ); + $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $expiry, $block->getExpiry() ); + $this->assertFalse( $block->isSitewide() ); + $this->assertCount( 1, $block->getRestrictions() ); + $this->assertTrue( BlockRestriction::equals( $block->getRestrictions(), [ + new PageRestriction( $block->getId(), $pageMars->getId() ), + ] ) ); + + // Remove the last page from the block. + $data['PageRestrictions'] = ''; + $result = $page->processForm( $data, $context ); + + $this->assertTrue( $result ); + + $block = Block::newFromTarget( $badActor ); + $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $expiry, $block->getExpiry() ); + $this->assertFalse( $block->isSitewide() ); + $this->assertCount( 0, $block->getRestrictions() ); + + // Change to sitewide. + $data['EditingRestriction'] = 'sitewide'; + $result = $page->processForm( $data, $context ); + + $this->assertTrue( $result ); + + $block = Block::newFromTarget( $badActor ); + $this->assertSame( $reason, $block->mReason ); + $this->assertSame( $expiry, $block->getExpiry() ); + $this->assertTrue( $block->isSitewide() ); + $this->assertCount( 0, $block->getRestrictions() ); + + // Ensure that there are no restrictions where the blockId is 0. + $count = $this->db->selectRowCount( + 'ipblocks_restrictions', + '*', + [ 'ir_ipb_id' => 0 ], + __METHOD__ + ); + $this->assertSame( 0, $count ); + } + + protected function insertBlock() { + $badActor = $this->getTestUser()->getUser(); + $sysop = $this->getTestSysop()->getUser(); + + $block = new \Block( [ + 'address' => $badActor->getName(), + 'user' => $badActor->getId(), + 'by' => $sysop->getId(), + 'expiry' => 'infinity', + 'sitewide' => 1, + 'enableAutoblock' => true, + ] ); + + $block->insert(); + + return $block; + } + + protected function resetTables() { + $this->db->delete( 'ipblocks', '*', __METHOD__ ); + $this->db->delete( 'ipblocks_restrictions', '*', __METHOD__ ); + } +} -- 2.20.1