mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-11-27 15:30:42 +00:00
Merge "Refactor ConsequencesExecutor to process consequences in more steps"
This commit is contained in:
commit
def507f6d3
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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' );
|
||||
|
|
|
@ -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 ],
|
||||
|
|
|
@ -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
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
255
tests/phpunit/unit/Consequences/ConsequencesExecutorTest.php
Normal file
255
tests/phpunit/unit/Consequences/ConsequencesExecutorTest.php
Normal 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
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue