Merge "Refactor ConsequencesExecutor to process consequences in more steps"

This commit is contained in:
jenkins-bot 2022-03-23 09:06:55 +00:00 committed by Gerrit Code Review
commit def507f6d3
6 changed files with 476 additions and 338 deletions

View file

@ -108,4 +108,12 @@ class Block extends BlockingConsequence implements ReversibleConsequence {
GlobalNameUtils::buildGlobalName( $filter->getID(), $this->parameters->getIsGlobalFilter() )
];
}
/**
* @return string
* @internal
*/
public function getExpiry(): string {
return $this->expiry;
}
}

View file

@ -4,6 +4,7 @@ namespace MediaWiki\Extension\AbuseFilter\Consequences;
use MediaWiki\Block\BlockUser;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Block;
use MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Consequence;
use MediaWiki\Extension\AbuseFilter\Consequences\Consequence\ConsequencesDisablerConsequence;
use MediaWiki\Extension\AbuseFilter\Consequences\Consequence\HookAborterConsequence;
@ -87,9 +88,7 @@ class ConsequencesExecutor {
* the errors and warnings to be shown to the user to explain the actions.
*/
public function executeFilterActions( array $filters ): Status {
$actionsByFilter = $this->consLookup->getConsequencesForFilters( $filters );
$consequences = $this->replaceArraysWithConsequences( $actionsByFilter );
$actionsToTake = $this->getFilteredConsequences( $consequences );
$actionsToTake = $this->getActualConsequencesToExecute( $filters );
$actionsTaken = array_fill_keys( $filters, [] );
$messages = [];
@ -110,104 +109,208 @@ class ConsequencesExecutor {
}
/**
* Remove consequences that we already know won't be executed. This includes:
* - Only keep the longest block from all filters
* - For global filters, remove locally disabled actions
* - For every filter, remove "disallow" if a blocking action will be executed
* Then, convert the remaining ones to Consequence objects.
*
* @param array[] $actionsByFilter
* @param string[] $filters
* @return Consequence[][]
* @internal Temporarily public
* @internal
*/
public function replaceArraysWithConsequences( array $actionsByFilter ): array {
// Keep track of the longest block
$maxBlock = [ 'id' => null, 'expiry' => -1, 'blocktalk' => null ];
$dangerousActions = $this->consRegistry->getDangerousActionNames();
public function getActualConsequencesToExecute( array $filters ): array {
$rawConsParamsByFilter = $this->consLookup->getConsequencesForFilters( $filters );
$consParamsByFilter = $this->replaceLegacyParameters( $rawConsParamsByFilter );
$specializedConsParams = $this->specializeParameters( $consParamsByFilter );
$allowedConsParams = $this->removeForbiddenConsequences( $specializedConsParams );
foreach ( $actionsByFilter as $filter => &$actions ) {
$isGlobalFilter = GlobalNameUtils::splitGlobalName( $filter )[1];
if ( $isGlobalFilter ) {
$actions = array_diff_key(
$actions,
array_filter( $this->options->get( 'AbuseFilterLocallyDisabledGlobalActions' ) )
);
}
// Don't show the disallow message if a blocking action is executed
if ( array_intersect( array_keys( $actions ), $dangerousActions )
&& isset( $actions['disallow'] )
) {
unset( $actions['disallow'] );
}
$consequences = $this->replaceArraysWithConsequences( $allowedConsParams );
$actualConsequences = $this->applyConsequenceDisablers( $consequences );
$deduplicatedConsequences = $this->deduplicateConsequences( $actualConsequences );
return $this->removeRedundantConsequences( $deduplicatedConsequences );
}
/**
* Update parameters for all consequences, making sure that they match the currently expected format
* (e.g., 'block' didn't use to have expiries).
*
* @param array[] $consParams
* @return array[]
*/
private function replaceLegacyParameters( array $consParams ): array {
$registeredBlockDuration = $this->options->get( 'AbuseFilterBlockDuration' );
$anonBlockDuration = $this->options->get( 'AbuseFilterAnonBlockDuration' ) ?? $registeredBlockDuration;
foreach ( $consParams as $filter => $actions ) {
foreach ( $actions as $name => $parameters ) {
switch ( $name ) {
case 'throttle':
case 'warn':
case 'disallow':
case 'rangeblock':
case 'degroup':
case 'blockautopromote':
case 'tag':
$actions[$name] = $this->actionsParamsToConsequence( $name, $parameters, $filter );
break;
case 'block':
// TODO Move to a dedicated method and/or create a generic interface
if ( count( $parameters ) === 3 ) {
// New type of filters with custom block
if ( $this->user->isAnon() ) {
$expiry = $parameters[1];
} else {
$expiry = $parameters[2];
}
} else {
// Old type with fixed expiry
$anonDuration = $this->options->get( 'AbuseFilterAnonBlockDuration' );
if ( $anonDuration !== null && $this->user->isAnon() ) {
// The user isn't logged in and the anon block duration
// doesn't default to $wgAbuseFilterBlockDuration.
$expiry = $anonDuration;
} else {
$expiry = $this->options->get( 'AbuseFilterBlockDuration' );
}
}
if ( $name === 'block' && count( $parameters ) !== 3 ) {
// Old type with fixed expiry
$blockTalk = in_array( 'blocktalk', $parameters, true );
$parsedExpiry = BlockUser::parseExpiryInput( $expiry );
if (
$maxBlock['expiry'] === -1 ||
$parsedExpiry > BlockUser::parseExpiryInput( $maxBlock['expiry'] )
) {
// Save the parameters to issue the block with
$maxBlock = [
'id' => $filter,
'expiry' => $expiry,
'blocktalk' => is_array( $parameters ) && in_array( 'blocktalk', $parameters )
];
}
// We'll re-add it later
unset( $actions['block'] );
break;
default:
$cons = $this->actionsParamsToConsequence( $name, $parameters, $filter );
if ( $cons !== null ) {
$actions[$name] = $cons;
} else {
unset( $actions[$name] );
}
$consParams[$filter][$name] = [
$blockTalk ? 'blocktalk' : 'noTalkBlockSet',
$anonBlockDuration,
$registeredBlockDuration
];
}
}
}
unset( $actions );
if ( $maxBlock['id'] !== null ) {
$id = $maxBlock['id'];
unset( $maxBlock['id'] );
$actionsByFilter[$id]['block'] = $this->actionsParamsToConsequence( 'block', $maxBlock, $id );
return $consParams;
}
/**
* For every consequence, keep only the parameters that are relevant for this specific action being filtered.
* For instance, choose between anon expiry and registered expiry for blocks.
*
* @param array[] $consParams
* @return array[]
*/
private function specializeParameters( array $consParams ): array {
foreach ( $consParams as $filter => $actions ) {
foreach ( $actions as $name => $parameters ) {
if ( $name === 'block' ) {
$consParams[$filter][$name] = [
'expiry' => $this->user->isAnon() ? $parameters[1] : $parameters[2],
'blocktalk' => $parameters[0] === 'blocktalk'
];
}
}
}
return $actionsByFilter;
return $consParams;
}
/**
* Removes any consequence that cannot be executed. For instance, remove locally disabled
* consequences for global filters.
*
* @param array[] $consParams
* @return array[]
*/
private function removeForbiddenConsequences( array $consParams ): array {
$locallyDisabledActions = $this->options->get( 'AbuseFilterLocallyDisabledGlobalActions' );
foreach ( $consParams as $filter => $actions ) {
$isGlobalFilter = GlobalNameUtils::splitGlobalName( $filter )[1];
if ( $isGlobalFilter ) {
$consParams[$filter] = array_diff_key(
$actions,
array_filter( $locallyDisabledActions )
);
}
}
return $consParams;
}
/**
* Converts all consequence specifiers to Consequence objects.
*
* @param array[] $actionsByFilter
* @return Consequence[][]
*/
private function replaceArraysWithConsequences( array $actionsByFilter ): array {
$ret = [];
foreach ( $actionsByFilter as $filter => $actions ) {
$ret[$filter] = [];
foreach ( $actions as $name => $parameters ) {
$cons = $this->actionsParamsToConsequence( $name, $parameters, $filter );
if ( $cons !== null ) {
$ret[$filter][$name] = $cons;
}
}
}
return $ret;
}
/**
* Pre-check any consequences-disabler consequence and remove any further actions prevented by them. Specifically:
* - For every filter with "throttle" enabled, remove other actions if the throttle counter hasn't been reached
* - For every filter with "warn" enabled, remove other actions if the warning hasn't been shown
*
* @param Consequence[][] $consequencesByFilter
* @return Consequence[][]
*/
private function applyConsequenceDisablers( array $consequencesByFilter ): array {
foreach ( $consequencesByFilter as $filter => $actions ) {
/** @var ConsequencesDisablerConsequence[] $consequenceDisablers */
$consequenceDisablers = array_filter( $actions, static function ( $el ) {
return $el instanceof ConsequencesDisablerConsequence;
} );
'@phan-var ConsequencesDisablerConsequence[] $consequenceDisablers';
uasort(
$consequenceDisablers,
static function ( ConsequencesDisablerConsequence $x, ConsequencesDisablerConsequence $y ) {
return $x->getSort() - $y->getSort();
}
);
foreach ( $consequenceDisablers as $name => $consequence ) {
if ( $consequence->shouldDisableOtherConsequences() ) {
$consequencesByFilter[$filter] = [ $name => $consequence ];
continue 2;
}
}
}
return $consequencesByFilter;
}
/**
* Removes duplicated consequences. For instance, this only keeps the longest of all blocks.
*
* @param Consequence[][] $consByFilter
* @return Consequence[][]
*/
private function deduplicateConsequences( array $consByFilter ): array {
// Keep track of the longest block
$maxBlock = [ 'id' => null, 'expiry' => -1, 'cons' => null ];
foreach ( $consByFilter as $filter => $actions ) {
foreach ( $actions as $name => $cons ) {
if ( $name === 'block' ) {
/** @var Block $cons */
'@phan-var Block $cons';
$expiry = $cons->getExpiry();
$parsedExpiry = BlockUser::parseExpiryInput( $expiry );
if (
$maxBlock['expiry'] === -1 ||
$parsedExpiry > BlockUser::parseExpiryInput( $maxBlock['expiry'] )
) {
$maxBlock = [
'id' => $filter,
'expiry' => $expiry,
'cons' => $cons
];
}
// We'll re-add it later
unset( $consByFilter[$filter]['block'] );
}
}
}
if ( $maxBlock['id'] !== null ) {
$consByFilter[$maxBlock['id']]['block'] = $maxBlock['cons'];
}
return $consByFilter;
}
/**
* Remove redundant consequences, e.g., remove "disallow" if a dangerous action will be executed
* TODO: Is this wanted, especially now that we have custom disallow messages?
*
* @param Consequence[][] $consByFilter
* @return Consequence[][]
*/
private function removeRedundantConsequences( array $consByFilter ): array {
$dangerousActions = $this->consRegistry->getDangerousActionNames();
foreach ( $consByFilter as $filter => $actions ) {
// Don't show the disallow message if a blocking action is executed
if (
isset( $actions['disallow'] ) &&
array_intersect( array_keys( $actions ), $dangerousActions )
) {
unset( $consByFilter[$filter]['disallow'] );
}
}
return $consByFilter;
}
/**
@ -253,7 +356,11 @@ class ConsequencesExecutor {
$duration = $this->options->get( 'AbuseFilterBlockAutopromoteDuration' ) * 86400;
return $this->consFactory->newBlockAutopromote( $baseConsParams, $duration );
case 'block':
return $this->consFactory->newBlock( $baseConsParams, $rawParams['expiry'], $rawParams['blocktalk'] );
return $this->consFactory->newBlock(
$baseConsParams,
$rawParams['expiry'],
$rawParams['blocktalk']
);
case 'tag':
try {
// The variable is not lazy-loaded
@ -273,40 +380,6 @@ class ConsequencesExecutor {
}
}
/**
* Pre-check any "special" consequence and remove any further actions prevented by them. Specifically:
* should be actually executed. Normalizations done here:
* - For every filter with "throttle" enabled, remove other actions if the throttle counter hasn't been reached
* - For every filter with "warn" enabled, remove other actions if the warning hasn't been shown
*
* @param Consequence[][] $actionsByFilter
* @return Consequence[][]
* @internal Temporary method
*/
public function getFilteredConsequences( array $actionsByFilter ): array {
foreach ( $actionsByFilter as $filter => $actions ) {
/** @var ConsequencesDisablerConsequence[] $consequenceDisablers */
$consequenceDisablers = array_filter( $actions, static function ( $el ) {
return $el instanceof ConsequencesDisablerConsequence;
} );
'@phan-var ConsequencesDisablerConsequence[] $consequenceDisablers';
uasort(
$consequenceDisablers,
static function ( ConsequencesDisablerConsequence $x, ConsequencesDisablerConsequence $y ) {
return $x->getSort() - $y->getSort();
}
);
foreach ( $consequenceDisablers as $name => $consequence ) {
if ( $consequence->shouldDisableOtherConsequences() ) {
$actionsByFilter[$filter] = [ $name => $consequence ];
continue 2;
}
}
}
return $actionsByFilter;
}
/**
* @param Consequence $consequence
* @return array [ Executed (bool), Message (?array) ] The message is given as an array

View file

@ -1298,6 +1298,7 @@ class AbuseFilterViewEdit extends AbuseFilterView {
$parameters[0] = $specMsg;
} elseif ( $action === 'block' ) {
// TODO: Should save a boolean
$parameters[0] = $request->getCheck( 'wpFilterBlockTalk' ) ?
'blocktalk' : 'noTalkBlockSet';
$parameters[1] = $request->getVal( 'wpBlockAnonDuration' );

View file

@ -643,25 +643,23 @@ class AbuseFilterConsequencesTest extends MediaWikiIntegrationTestCase {
*
* @param Status $result As returned by self::doAction
* @param array $actionParams As it's given by data providers
* @param array $consequences As it's given by data providers
* @param array $expectedConsequences
* @return array [ expected consequences, actual consequences ]
*/
private function checkConsequences( $result, $actionParams, $consequences ) {
global $wgAbuseFilterActionRestrictions;
private function checkConsequences( $result, $actionParams, $expectedConsequences ) {
$expectedErrors = [];
$testErrorMessage = false;
foreach ( $consequences as $consequence => $ids ) {
foreach ( $expectedConsequences as $consequence => $ids ) {
foreach ( $ids as $id ) {
$params = self::$filters[$id]['actions'][$consequence];
switch ( $consequence ) {
case 'warn':
// Aborts the hook with the warning message as error.
$expectedErrors['warn'][] = $params[0] ?? 'abusefilter-warning';
$expectedErrors[] = $params[0] ?? 'abusefilter-warning';
break;
case 'disallow':
// Aborts the hook with the disallow message error.
$expectedErrors['disallow'][] = $params[0] ?? 'abusefilter-disallowed';
$expectedErrors[] = $params[0] ?? 'abusefilter-disallowed';
break;
case 'block':
// Aborts the hook with 'abusefilter-blocked-display' error. Should block
@ -693,11 +691,11 @@ class AbuseFilterConsequencesTest extends MediaWikiIntegrationTestCase {
break;
}
$expectedErrors['block'][] = 'abusefilter-blocked-display';
$expectedErrors[] = 'abusefilter-blocked-display';
break;
case 'degroup':
// Aborts the hook with 'abusefilter-degrouped' error and degroups the user.
$expectedErrors['degroup'][] = 'abusefilter-degrouped';
$expectedErrors[] = 'abusefilter-degrouped';
$ugm = MediaWikiServices::getInstance()->getUserGroupManager();
$groupCheck = !in_array( 'sysop', $ugm->getUserEffectiveGroups( $this->user ) );
if ( !$groupCheck ) {
@ -720,7 +718,7 @@ class AbuseFilterConsequencesTest extends MediaWikiIntegrationTestCase {
throw new UnexpectedValueException( 'Use self::testThrottleConsequence to test throttling' );
case 'blockautopromote':
// Aborts the hook with 'abusefilter-autopromote-blocked' error and prevent promotion.
$expectedErrors['blockautopromote'][] = 'abusefilter-autopromote-blocked';
$expectedErrors[] = 'abusefilter-autopromote-blocked';
$value = AbuseFilterServices::getBlockAutopromoteStore()
->getAutoPromoteBlockStatus( $this->user );
if ( !$value ) {
@ -737,22 +735,6 @@ class AbuseFilterConsequencesTest extends MediaWikiIntegrationTestCase {
}
}
if ( array_intersect_key( $expectedErrors, array_filter( $wgAbuseFilterActionRestrictions ) ) ) {
$filteredExpected = array_intersect_key(
$expectedErrors,
array_filter( $wgAbuseFilterActionRestrictions )
);
$expected = [];
foreach ( $filteredExpected as $values ) {
$expected = array_merge( $expected, $values );
}
} else {
$expected = $expectedErrors['warn'] ?? $expectedErrors['disallow'] ?? null;
if ( !is_array( $expected ) ) {
$expected = (array)$expected;
}
}
$errors = $result->getErrors();
$actual = [];
@ -765,9 +747,9 @@ class AbuseFilterConsequencesTest extends MediaWikiIntegrationTestCase {
}
}
sort( $expected );
sort( $expectedErrors );
sort( $actual );
return [ $expected, $actual ];
return [ $expectedErrors, $actual ];
}
/**
@ -775,13 +757,13 @@ class AbuseFilterConsequencesTest extends MediaWikiIntegrationTestCase {
*
* @param int[] $createIds IDs of the filters to create
* @param array $actionParams Details of the action we need to execute to trigger filters
* @param array $consequences The consequences we're expecting
* @param array $expectedConsequences The consequences we're expecting
* @dataProvider provideFilters
*/
public function testFilterConsequences( $createIds, $actionParams, $consequences ) {
public function testFilterConsequences( $createIds, $actionParams, $expectedConsequences ) {
$this->createFilters( $createIds );
$result = $this->doAction( $actionParams );
list( $expected, $actual ) = $this->checkConsequences( $result, $actionParams, $consequences );
list( $expected, $actual ) = $this->checkConsequences( $result, $actionParams, $expectedConsequences );
$this->assertEquals(
$expected,
@ -820,7 +802,7 @@ class AbuseFilterConsequencesTest extends MediaWikiIntegrationTestCase {
'target' => 'Test page',
'newTitle' => 'Another test page'
],
[ 'disallow' => [ 2 ], 'block' => [ 2 ] ]
[ 'block' => [ 2 ] ]
],
'Basic test for "delete" action' => [
[ 2, 3 ],
@ -1520,7 +1502,7 @@ class AbuseFilterConsequencesTest extends MediaWikiIntegrationTestCase {
'newText' => 'New text',
'summary' => ''
],
[ 'disallow' => [ 18 ], 'warn' => [ 18 ] ]
[ 'warn' => [ 18 ] ]
],
[
[ 19 ],

View file

@ -1,181 +0,0 @@
<?php
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutor;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesLookup;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry;
use MediaWiki\Extension\AbuseFilter\Filter\ExistingFilter;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use Psr\Log\NullLogger;
use Wikimedia\TestingAccessWrapper;
/**
* @group Test
* @group AbuseFilter
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutor
*/
class ConsequencesExecutorTest extends MediaWikiIntegrationTestCase {
/**
* @param array $rawConsequences A raw, unfiltered list of consequences
* @param array $expectedKeys
* @param Title $title
* @covers \MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutor
* @dataProvider provideConsequences
* @todo Rewrite this test
*/
public function testGetFilteredConsequences( $rawConsequences, $expectedKeys, Title $title ) {
$locallyDisabledActions = [
'flag' => false,
'throttle' => false,
'warn' => false,
'disallow' => false,
'blockautopromote' => true,
'block' => true,
'rangeblock' => true,
'degroup' => true,
'tag' => false
];
$options = $this->createMock( ServiceOptions::class );
$options->method( 'get' )
->with( 'AbuseFilterLocallyDisabledGlobalActions' )
->willReturn( $locallyDisabledActions );
$fakeFilter = $this->createMock( ExistingFilter::class );
$fakeFilter->method( 'getName' )->willReturn( 'unused name' );
$fakeFilter->method( 'getID' )->willReturn( 1 );
$fakeLookup = $this->createMock( FilterLookup::class );
$fakeLookup->method( 'getFilter' )->willReturn( $fakeFilter );
$consRegistry = $this->createMock( ConsequencesRegistry::class );
$dangerousActions = TestingAccessWrapper::constant( ConsequencesRegistry::class, 'DANGEROUS_ACTIONS' );
$consRegistry->method( 'getDangerousActionNames' )->willReturn( $dangerousActions );
$user = $this->createMock( User::class );
$vars = VariableHolder::newFromArray( [ 'action' => 'edit' ] );
$executor = new ConsequencesExecutor(
$this->createMock( ConsequencesLookup::class ),
AbuseFilterServices::getConsequencesFactory(),
$consRegistry,
$fakeLookup,
new NullLogger,
$options,
$user,
$title,
$vars
);
$actual = $executor->getFilteredConsequences(
$executor->replaceArraysWithConsequences( $rawConsequences ) );
$actualKeys = [];
foreach ( $actual as $filter => $actions ) {
$actualKeys[$filter] = array_keys( $actions );
}
$this->assertEquals( $expectedKeys, $actualKeys );
}
/**
* Data provider for testGetFilteredConsequences
* @todo Split these
* @return array
*/
public function provideConsequences() {
$pageName = 'TestFilteredConsequences';
$title = $this->createMock( Title::class );
$title->method( 'getPrefixedText' )->willReturn( $pageName );
return [
'warn and throttle exclude other actions' => [
[
2 => [
'warn' => [
'abusefilter-warning'
],
'tag' => [
'some tag'
]
],
13 => [
'throttle' => [
'13',
'14,15',
'user'
],
'disallow' => []
],
168 => [
'degroup' => []
]
],
[
2 => [ 'warn' ],
13 => [ 'throttle' ],
168 => [ 'degroup' ]
],
$title
],
'warn excludes other actions, block excludes disallow' => [
[
3 => [
'tag' => [
'some tag'
]
],
'global-2' => [
'warn' => [
'abusefilter-beautiful-warning'
],
'degroup' => []
],
4 => [
'disallow' => [],
'block' => [
'blocktalk',
'15 minutes',
'indefinite'
]
]
],
[
3 => [ 'tag' ],
'global-2' => [ 'warn' ],
4 => [ 'block' ]
],
$title
],
'some global actions are disabled locally, the longest block is chosen' => [
[
'global-1' => [
'blockautopromote' => [],
'block' => [
'blocktalk',
'indefinite',
'indefinite'
]
],
1 => [
'block' => [
'blocktalk',
'4 hours',
'4 hours'
]
],
2 => [
'degroup' => [],
'block' => [
'blocktalk',
'infinity',
'never'
]
]
],
[
'global-1' => [],
1 => [],
2 => [ 'degroup', 'block' ]
],
$title
],
];
}
}

View file

@ -0,0 +1,255 @@
<?php
namespace MediaWiki\Extension\AbuseFilter\Tests\Unit\Consequences;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Block;
use MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Throttle;
use MediaWiki\Extension\AbuseFilter\Consequences\Consequence\Warn;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutor;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesFactory;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesLookup;
use MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesRegistry;
use MediaWiki\Extension\AbuseFilter\Consequences\Parameters;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWikiUnitTestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\NullLogger;
use Title;
use User;
use Wikimedia\TestingAccessWrapper;
/**
* @group Test
* @group AbuseFilter
* @coversDefaultClass \MediaWiki\Extension\AbuseFilter\Consequences\ConsequencesExecutor
*/
class ConsequencesExecutorTest extends MediaWikiUnitTestCase {
/**
* Returns a ConsequencesFactory where:
* - all the ConsequenceDisablerConsequence's created will disable other consequences.
* - the block expiry is set as normal
* @return ConsequencesFactory|MockObject
*/
private function getConsequencesFactory() {
$consFactory = $this->createMock( ConsequencesFactory::class );
$warn = $this->createMock( Warn::class );
$warn->method( 'shouldDisableOtherConsequences' )->willReturn( true );
$consFactory->method( 'newWarn' )->willReturn( $warn );
$throttle = $this->createMock( Throttle::class );
$throttle->method( 'shouldDisableOtherConsequences' )->willReturn( true );
$consFactory->method( 'newThrottle' )->willReturn( $throttle );
$consFactory->method( 'newBlock' )->willReturnCallback(
function ( Parameters $params, string $expiry, bool $preventsTalk ): Block {
$block = $this->createMock( Block::class );
$block->method( 'getExpiry' )->willReturn( $expiry );
return $block;
}
);
return $consFactory;
}
private function getConsExecutor( array $consequences, Title $title ): ConsequencesExecutor {
$locallyDisabledActions = [
'flag' => false,
'throttle' => false,
'warn' => false,
'disallow' => false,
'blockautopromote' => true,
'block' => true,
'rangeblock' => true,
'degroup' => true,
'tag' => false
];
$options = new ServiceOptions(
ConsequencesExecutor::CONSTRUCTOR_OPTIONS,
[
'AbuseFilterLocallyDisabledGlobalActions' => $locallyDisabledActions,
'AbuseFilterBlockDuration' => '24 hours',
'AbuseFilterAnonBlockDuration' => '24 hours',
'AbuseFilterBlockAutopromoteDuration' => '5 days',
]
);
$consRegistry = $this->createMock( ConsequencesRegistry::class );
$dangerousActions = TestingAccessWrapper::constant( ConsequencesRegistry::class, 'DANGEROUS_ACTIONS' );
$consRegistry->method( 'getDangerousActionNames' )->willReturn( $dangerousActions );
$consLookup = $this->createMock( ConsequencesLookup::class );
$consLookup->expects( $this->atLeastOnce() )
->method( 'getConsequencesForFilters' )
->with( array_keys( $consequences ) )
->willReturn( $consequences );
return new ConsequencesExecutor(
$consLookup,
$this->getConsequencesFactory(),
$consRegistry,
$this->createMock( FilterLookup::class ),
new NullLogger,
$options,
$this->createMock( User::class ),
$title,
VariableHolder::newFromArray( [ 'action' => 'edit' ] )
);
}
/**
* @param array $rawConsequences A raw, unfiltered list of consequences
* @param array $expectedKeys
* @param Title $title
*
* @covers ::getActualConsequencesToExecute
* @covers ::replaceLegacyParameters
* @covers ::specializeParameters
* @covers ::removeForbiddenConsequences
* @covers ::replaceArraysWithConsequences
* @covers ::applyConsequenceDisablers
* @covers ::deduplicateConsequences
* @covers ::removeRedundantConsequences
* @dataProvider provideConsequences
*/
public function testGetActualConsequencesToExecute(
array $rawConsequences,
array $expectedKeys,
Title $title
): void {
$executor = $this->getConsExecutor( $rawConsequences, $title );
$actual = $executor->getActualConsequencesToExecute( array_keys( $rawConsequences ) );
$actualKeys = [];
foreach ( $actual as $filter => $actions ) {
$actualKeys[$filter] = array_keys( $actions );
}
$this->assertEquals( $expectedKeys, $actualKeys );
}
/**
* @return array
*/
public function provideConsequences(): array {
$pageName = 'TestFilteredConsequences';
$title = $this->createMock( Title::class );
$title->method( 'getPrefixedText' )->willReturn( $pageName );
return [
'warn and throttle exclude other actions' => [
[
2 => [
'warn' => [
'abusefilter-warning'
],
'tag' => [
'some tag'
]
],
13 => [
'throttle' => [
'13',
'14,15',
'user'
],
'disallow' => []
],
168 => [
'degroup' => []
]
],
[
2 => [ 'warn' ],
13 => [ 'throttle' ],
168 => [ 'degroup' ]
],
$title
],
'warn excludes other actions, block excludes disallow' => [
[
3 => [
'tag' => [
'some tag'
]
],
'global-2' => [
'warn' => [
'abusefilter-beautiful-warning'
],
'degroup' => []
],
4 => [
'disallow' => [],
'block' => [
'blocktalk',
'15 minutes',
'indefinite'
]
]
],
[
3 => [ 'tag' ],
'global-2' => [ 'warn' ],
4 => [ 'block' ]
],
$title
],
'some global actions are disabled locally, the longest block is chosen' => [
[
'global-1' => [
'blockautopromote' => [],
'block' => [
'blocktalk',
'indefinite',
'indefinite'
]
],
1 => [
'block' => [
'blocktalk',
'4 hours',
'4 hours'
]
],
2 => [
'degroup' => [],
'block' => [
'blocktalk',
'infinity',
'never'
]
]
],
[
'global-1' => [],
1 => [],
2 => [ 'degroup', 'block' ]
],
$title
],
'do not use a block that will be skipped as the longer one' => [
[
1 => [
'warn' => [
'abusefilter-warning'
],
'block' => [
'blocktalk',
'4 hours',
'4 hours'
]
],
2 => [
'block' => [
'blocktalk',
'2 hours',
'2 hours'
]
]
],
[
1 => [ 'warn' ],
2 => [ 'block' ]
],
$title
],
];
}
}