diff options
-rw-r--r-- | includes/specialpage/SpecialPageFactory.php | 1 | ||||
-rw-r--r-- | includes/specials/SpecialBlockList.php | 54 | ||||
-rw-r--r-- | languages/i18n/en.json | 2 | ||||
-rw-r--r-- | languages/i18n/qqq.json | 2 | ||||
-rw-r--r-- | tests/phpunit/integration/includes/specials/SpecialBlockListTest.php | 172 |
5 files changed, 221 insertions, 10 deletions
diff --git a/includes/specialpage/SpecialPageFactory.php b/includes/specialpage/SpecialPageFactory.php index e91d174049e9..aed9cb398a3a 100644 --- a/includes/specialpage/SpecialPageFactory.php +++ b/includes/specialpage/SpecialPageFactory.php @@ -552,6 +552,7 @@ class SpecialPageFactory { 'HideUserUtils', 'BlockActionInfo', 'RowCommentFormatter', + 'TempUserConfig', ], ], 'AutoblockList' => [ diff --git a/includes/specials/SpecialBlockList.php b/includes/specials/SpecialBlockList.php index 1af7b9c3f32c..1f6f3e6189e7 100644 --- a/includes/specials/SpecialBlockList.php +++ b/includes/specials/SpecialBlockList.php @@ -33,8 +33,10 @@ use MediaWiki\Html\Html; use MediaWiki\HTMLForm\HTMLForm; use MediaWiki\Pager\BlockListPager; use MediaWiki\SpecialPage\SpecialPage; +use MediaWiki\User\TempUser\TempUserConfig; use Wikimedia\IPUtils; use Wikimedia\Rdbms\IConnectionProvider; +use Wikimedia\Rdbms\IExpression; use Wikimedia\Rdbms\IReadableDatabase; /** @@ -63,6 +65,7 @@ class SpecialBlockList extends SpecialPage { private HideUserUtils $hideUserUtils; private BlockActionInfo $blockActionInfo; private RowCommentFormatter $rowCommentFormatter; + private TempUserConfig $tempUserConfig; public function __construct( LinkBatchFactory $linkBatchFactory, @@ -73,7 +76,8 @@ class SpecialBlockList extends SpecialPage { BlockUtils $blockUtils, HideUserUtils $hideUserUtils, BlockActionInfo $blockActionInfo, - RowCommentFormatter $rowCommentFormatter + RowCommentFormatter $rowCommentFormatter, + TempUserConfig $tempUserConfig ) { parent::__construct( 'BlockList' ); @@ -86,6 +90,7 @@ class SpecialBlockList extends SpecialPage { $this->hideUserUtils = $hideUserUtils; $this->blockActionInfo = $blockActionInfo; $this->rowCommentFormatter = $rowCommentFormatter; + $this->tempUserConfig = $tempUserConfig; } /** @@ -119,6 +124,22 @@ class SpecialBlockList extends SpecialPage { // Setup BlockListPager here to get the actual default Limit $pager = $this->getBlockListPager(); + $blockFilterOptions = [ + 'blocklist-tempblocks' => 'tempblocks', + 'blocklist-indefblocks' => 'indefblocks', + 'blocklist-autoblocks' => 'autoblocks', + 'blocklist-addressblocks' => 'addressblocks', + 'blocklist-rangeblocks' => 'rangeblocks', + ]; + + if ( $this->tempUserConfig->isKnown() ) { + // Clarify that "userblocks" excludes named users only if temporary accounts are known (T380266) + $blockFilterOptions['blocklist-nameduserblocks'] = 'userblocks'; + $blockFilterOptions['blocklist-tempuserblocks'] = 'tempuserblocks'; + } else { + $blockFilterOptions['blocklist-userblocks'] = 'userblocks'; + } + // Just show the block list $fields = [ 'Target' => [ @@ -130,14 +151,7 @@ class SpecialBlockList extends SpecialPage { ], 'Options' => [ 'type' => 'multiselect', - 'options-messages' => [ - 'blocklist-tempblocks' => 'tempblocks', - 'blocklist-indefblocks' => 'indefblocks', - 'blocklist-autoblocks' => 'autoblocks', - 'blocklist-userblocks' => 'userblocks', - 'blocklist-addressblocks' => 'addressblocks', - 'blocklist-rangeblocks' => 'rangeblocks', - ], + 'options-messages' => $blockFilterOptions, 'flatlist' => true, ], ]; @@ -214,7 +228,17 @@ class SpecialBlockList extends SpecialPage { // Apply filters if ( in_array( 'userblocks', $this->options ) ) { - $conds['bt_user'] = null; + $namedUserConds = $db->expr( 'bt_user', '=', null ); + + // If temporary accounts are a known concept on this wiki, + // have the "Hide account blocks" filter exclude only named users (T380266). + if ( $this->tempUserConfig->isKnown() ) { + $namedUserConds = $namedUserConds->orExpr( + $this->tempUserConfig->getMatchCondition( $db, 'bt_user_text', IExpression::LIKE ) + ); + } + + $conds[] = $namedUserConds; } if ( in_array( 'autoblocks', $this->options ) ) { $conds['bl_parent_block_id'] = null; @@ -230,6 +254,16 @@ class SpecialBlockList extends SpecialPage { $conds['bt_range_start'] = null; } + if ( + in_array( 'tempuserblocks', $this->options ) && + $this->tempUserConfig->isKnown() + ) { + $conds[] = $db->expr( 'bt_user', '=', null ) + ->orExpr( + $this->tempUserConfig->getMatchCondition( $db, 'bt_user_text', IExpression::NOT_LIKE ) + ); + } + $hideTemp = in_array( 'tempblocks', $this->options ); $hideIndef = in_array( 'indefblocks', $this->options ); if ( $hideTemp && $hideIndef ) { diff --git a/languages/i18n/en.json b/languages/i18n/en.json index 4806fa0088bb..f6ff4344c0ec 100644 --- a/languages/i18n/en.json +++ b/languages/i18n/en.json @@ -2735,9 +2735,11 @@ "ipblocklist-legend": "Find a blocked user", "blocklist-autoblocks": "Hide autoblocks", "blocklist-userblocks": "Hide account blocks", + "blocklist-nameduserblocks": "Hide named user blocks", "blocklist-tempblocks": "Hide temporary blocks", "blocklist-indefblocks": "Hide indefinite blocks", "blocklist-addressblocks": "Hide single IP blocks", + "blocklist-tempuserblocks": "Hide temporary account blocks", "blocklist-type": "Type:", "blocklist-type-header": "Type", "blocklist-type-opt-all": "All", diff --git a/languages/i18n/qqq.json b/languages/i18n/qqq.json index 01396ed37d1c..dd2464e90cd3 100644 --- a/languages/i18n/qqq.json +++ b/languages/i18n/qqq.json @@ -3002,6 +3002,8 @@ "blocklist-tempblocks": "Used as the label for the multi-select checkbox in the form on [[Special:BlockList]].\n{{Related|Blocklist-blocks}}", "blocklist-indefblocks": "Used as the label for the multi-select checkbox in the form on [[Special:BlockList]].\n{{Related|Blocklist-blocks}}", "blocklist-addressblocks": "Used as the label for the multi-select checkbox in the form on [[Special:BlockList]].\n{{Related|Blocklist-blocks}}", + "blocklist-nameduserblocks": "Used as the label for the multi-select checkbox in the form on [[Special:BlockList]].\n{{Related|Blocklist-blocks}}", + "blocklist-tempuserblocks": "Used as the label for the multi-select checkbox in the form on [[Special:BlockList]].\n{{Related|Blocklist-blocks}}", "blocklist-type": "Used as label for dropdown box in [[Special:BlockList]].\n\n{{Identical|Type}}", "blocklist-type-header": "Used as the header for the block log type column in the table on the page [[Special:Block]].", "blocklist-type-opt-all": "Used as option for dropdown box in [[Special:BlockList]]. This is the default option and indicates that \"all\" blocks will be listed\n{{Identical|All}}", diff --git a/tests/phpunit/integration/includes/specials/SpecialBlockListTest.php b/tests/phpunit/integration/includes/specials/SpecialBlockListTest.php new file mode 100644 index 000000000000..f597bbd01681 --- /dev/null +++ b/tests/phpunit/integration/includes/specials/SpecialBlockListTest.php @@ -0,0 +1,172 @@ +<?php +namespace MediaWiki\Tests\Specials; + +use HtmlFormatter\HtmlFormatter; +use MediaWiki\Request\FauxRequest; +use MediaWiki\Tests\User\TempUser\TempUserTestTrait; +use MediaWiki\User\UserIdentity; +use SpecialPageTestBase; +use Wikimedia\Parsoid\Utils\DOMCompat; + +/** + * @covers \MediaWiki\Specials\SpecialBlockList + * @group Database + */ +class SpecialBlockListTest extends SpecialPageTestBase { + use TempUserTestTrait; + + private const TEST_BLOCKED_IP = '127.0.0.1'; + + private static string $blockedTempUserName; + private static string $blockedUserName; + + protected function newSpecialPage() { + return $this->getServiceContainer()->getSpecialPageFactory()->getPage( 'BlockList' ); + } + + private function makeTestBlock( UserIdentity $target ): void { + $status = $this->getServiceContainer() + ->getBlockUserFactory() + ->newBlockUser( + $target, + $this->getTestSysop()->getAuthority(), + 'infinity' + ) + ->placeBlock(); + + $this->assertStatusGood( $status, "Failed to place block for {$target->getName()}" ); + } + + public function addDBDataOnce() { + $this->enableAutoCreateTempUser(); + + $req = new FauxRequest(); + $tempUser = $this->getServiceContainer() + ->getTempUserCreator() + ->create( null, $req ) + ->getUser(); + + $namedUser = $this->getTestUser()->getUser(); + + self::$blockedTempUserName = $tempUser->getName(); + self::$blockedUserName = $namedUser->getName(); + + $this->makeTestBlock( $tempUser ); + + $this->makeTestBlock( $this->getTestUser()->getUser() ); + + $this->makeTestBlock( + $this->getServiceContainer()->getUserFactory()->newAnonymous( self::TEST_BLOCKED_IP ) + ); + } + + /** + * @dataProvider provideTargetTypes + */ + public function testShouldAllowFilteringBlocksByTargetType( + ?string $targetType, + callable $expectedBlockTargetNamesProvider, + bool $tempAccountsKnown = true + ): void { + $this->disableAutoCreateTempUser( [ + 'known' => $tempAccountsKnown, + ] ); + + $queryParams = $targetType ? [ 'wpOptions' => [ $targetType ] ] : []; + $req = new FauxRequest( $queryParams ); + + [ $html ] = $this->executeSpecialPage( '', $req ); + + $doc = ( new HtmlFormatter( HtmlFormatter::wrapHTML( $html ) ) )->getDoc(); + $targetUserNames = []; + foreach ( DOMCompat::querySelectorAll( $doc, '.TablePager_col_target > .mw-userlink' ) as $targetUser ) { + $targetUserNames[] = $targetUser->textContent; + } + + $this->assertSame( + $expectedBlockTargetNamesProvider(), + $targetUserNames + ); + } + + public static function provideTargetTypes(): iterable { + yield 'no target type, temp accounts known' => [ + null, + static fn () => [ self::TEST_BLOCKED_IP, self::$blockedUserName, self::$blockedTempUserName ] + ]; + yield 'named user blocks excluded, temp accounts known' => [ + 'userblocks', + static fn () => [ self::TEST_BLOCKED_IP, self::$blockedTempUserName ] + ]; + yield 'IP blocks excluded, temp accounts known' => [ + 'addressblocks', + static fn () => [ self::$blockedUserName, self::$blockedTempUserName ] + ]; + yield 'temp user blocks excluded, temp accounts known' => [ + 'tempuserblocks', + static fn () => [ self::TEST_BLOCKED_IP, self::$blockedUserName ] + ]; + + yield 'no target type, temp accounts not known' => [ + null, + static fn () => [ self::TEST_BLOCKED_IP, self::$blockedUserName, self::$blockedTempUserName ], + false + ]; + yield 'user blocks excluded, temp accounts not known' => [ + 'userblocks', + static fn () => [ self::TEST_BLOCKED_IP ], + false + ]; + yield 'IP blocks excluded, temp accounts not known' => [ + 'addressblocks', + static fn () => [ self::$blockedUserName, self::$blockedTempUserName ], + false + ]; + } + + public function testShouldAdaptFilterOptionsWhenTemporaryAccountsAreKnown(): void { + $this->disableAutoCreateTempUser( [ + 'known' => true, + ] ); + + [ $html ] = $this->executeSpecialPage(); + + $doc = ( new HtmlFormatter( HtmlFormatter::wrapHTML( $html ) ) )->getDoc(); + + $accountBlocksFilter = DOMCompat::querySelector( $doc, 'input[name="wpOptions[]"][value="userblocks"]' ); + $accountBlocksLabel = DOMCompat::querySelector( + $doc, "label[for=\"{$accountBlocksFilter->getAttribute( 'id' )}\"]" + ); + + $this->assertSame( + '(blocklist-nameduserblocks)', + $accountBlocksLabel->textContent + ); + $this->assertNotNull( + DOMCompat::querySelector( $doc, 'input[name="wpOptions[]"][value="tempuserblocks"]' ), + 'Temporary accounts filter checkbox should be present' + ); + } + + public function testShouldNotShowTemporaryAccountsFilterCheckboxWhenTemporaryAccountsAreNotKnown(): void { + $this->disableAutoCreateTempUser(); + + [ $html ] = $this->executeSpecialPage(); + + $doc = ( new HtmlFormatter( HtmlFormatter::wrapHTML( $html ) ) )->getDoc(); + + $accountBlocksFilter = DOMCompat::querySelector( $doc, 'input[name="wpOptions[]"][value="userblocks"]' ); + $accountBlocksLabel = DOMCompat::querySelector( + $doc, "label[for=\"{$accountBlocksFilter->getAttribute( 'id' )}\"]" + ); + + $this->assertSame( + '(blocklist-userblocks)', + $accountBlocksLabel->textContent + ); + $this->assertNull( + DOMCompat::querySelector( $doc, 'input[name="wpOptions[]"][value="tempuserblocks"]' ), + 'Temporary accounts filter checkbox should not be present' + ); + } +} |