Introduce ConsequencesExecutor

This is the last big step towards moving Consequences-related things away from
AbuseFilterRunner. There's still some cleanup to do (+ write proper tests), but
this should really be the last important code change.

Change-Id: I347795fe93ba496c43b1d5cfc9ba6e1326842c06
This commit is contained in:
Daimona Eaytoy 2020-10-17 00:10:37 +02:00
parent f430cd211a
commit 68adaa5cb1
8 changed files with 496 additions and 307 deletions

View file

@ -182,6 +182,8 @@
"MediaWiki\\Extension\\AbuseFilter\\AbuseLogger": "includes/AbuseLogger.php",
"MediaWiki\\Extension\\AbuseFilter\\AbuseLoggerFactory": "includes/AbuseLoggerFactory.php",
"MediaWiki\\Extension\\AbuseFilter\\ConsequencesRegistry": "includes/ConsequencesRegistry.php",
"MediaWiki\\Extension\\AbuseFilter\\ConsequencesExecutor": "includes/ConsequencesExecutor.php",
"MediaWiki\\Extension\\AbuseFilter\\ConsequencesExecutorFactory": "includes/ConsequencesExecutorFactory.php",
"AFComputedVariable": "includes/AFComputedVariable.php",
"NormalizeThrottleParameters": "maintenance/normalizeThrottleParameters.php",
"FixOldLogEntries": "maintenance/fixOldLogEntries.php",

View file

@ -2,11 +2,6 @@
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagger;
use MediaWiki\Extension\AbuseFilter\Consequence\BCConsequence;
use MediaWiki\Extension\AbuseFilter\Consequence\Consequence;
use MediaWiki\Extension\AbuseFilter\Consequence\ConsequencesDisablerConsequence;
use MediaWiki\Extension\AbuseFilter\Consequence\HookAborterConsequence;
use MediaWiki\Extension\AbuseFilter\Consequence\Parameters;
use MediaWiki\Extension\AbuseFilter\Filter\Filter;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\AbuseFilter\FilterProfiler;
@ -228,7 +223,12 @@ class AbuseFilterRunner {
return Status::newGood();
}
$status = $this->executeFilterActions( $matchedFilters );
$executor = AbuseFilterServices::getConsequencesExecutorFactory()->newExecutor(
$this->user,
$this->title,
$this->vars
);
$status = $executor->executeFilterActions( $matchedFilters );
$actionsTaken = $status->getValue();
$abuseLogger = AbuseFilterServices::getAbuseLoggerFactory()->newLogger(
@ -454,290 +454,6 @@ class AbuseFilterRunner {
);
}
/**
* Executes a set of actions.
*
* @param string[] $filters
* @return Status returns the operation's status. $status->isOK() will return true if
* there were no actions taken, false otherwise. $status->getValue() will return
* an array listing the actions taken. $status->getErrors() etc. will provide
* the errors and warnings to be shown to the user to explain the actions.
*/
protected function executeFilterActions( array $filters ) : Status {
$consLookup = AbuseFilterServices::getConsequencesLookup();
$actionsByFilter = $consLookup->getConsequencesForFilters( $filters );
$consequences = $this->replaceArraysWithConsequences( $actionsByFilter );
$actionsToTake = $this->getFilteredConsequences( $consequences );
$actionsTaken = array_fill_keys( $filters, [] );
$messages = [];
foreach ( $actionsToTake as $filter => $actions ) {
foreach ( $actions as $action => $info ) {
[ $executed, $newMsg ] = $this->takeConsequenceAction( $info );
if ( $newMsg !== null ) {
$messages[] = $newMsg;
}
if ( $executed ) {
$actionsTaken[$filter][] = $action;
}
}
}
return $this->buildStatus( $actionsTaken, $messages );
}
/**
* 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
* @return Consequence[][]
* @internal Temporarily public
*/
public function replaceArraysWithConsequences( array $actionsByFilter ) : array {
global $wgAbuseFilterLocallyDisabledGlobalActions,
$wgAbuseFilterBlockDuration, $wgAbuseFilterAnonBlockDuration;
// Keep track of the longest block
$maxBlock = [ 'id' => null, 'expiry' => -1, 'blocktalk' => null ];
$dangerousActions = AbuseFilterServices::getConsequencesRegistry()->getDangerousActionNames();
foreach ( $actionsByFilter as $filter => &$actions ) {
$isGlobalFilter = GlobalNameUtils::splitGlobalName( $filter )[1];
if ( $isGlobalFilter ) {
$actions = array_diff_key( $actions, array_filter( $wgAbuseFilterLocallyDisabledGlobalActions ) );
}
// 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'] );
}
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
if ( $this->user->isAnon() && $wgAbuseFilterAnonBlockDuration !== null ) {
// The user isn't logged in and the anon block duration
// doesn't default to $wgAbuseFilterBlockDuration.
$expiry = $wgAbuseFilterAnonBlockDuration;
} else {
$expiry = $wgAbuseFilterBlockDuration;
}
}
$parsedExpiry = SpecialBlock::parseExpiryInput( $expiry );
if (
$maxBlock['expiry'] === -1 ||
$parsedExpiry > SpecialBlock::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] );
}
}
}
}
unset( $actions );
if ( $maxBlock['id'] !== null ) {
$id = $maxBlock['id'];
unset( $maxBlock['id'] );
$actionsByFilter[$id]['block'] = $this->actionsParamsToConsequence( 'block', $maxBlock, $id );
}
return $actionsByFilter;
}
/**
* @param string $actionName
* @param array $rawParams
* @param int|string $filter
* @return Consequence|null
*/
private function actionsParamsToConsequence( string $actionName, array $rawParams, $filter ) : ?Consequence {
global $wgAbuseFilterBlockAutopromoteDuration, $wgAbuseFilterCustomActionsHandlers;
[ $filterID, $isGlobalFilter ] = GlobalNameUtils::splitGlobalName( $filter );
$filterObj = $this->filterLookup->getFilter( $filterID, $isGlobalFilter );
$consFactory = AbuseFilterServices::getConsequencesFactory();
$baseConsParams = new Parameters(
$filterObj,
$isGlobalFilter,
$this->user,
$this->title,
$this->action
);
switch ( $actionName ) {
case 'throttle':
$throttleId = array_shift( $rawParams );
list( $rateCount, $ratePeriod ) = explode( ',', array_shift( $rawParams ) );
$throttleParams = [
'id' => $throttleId,
'count' => (int)$rateCount,
'period' => (int)$ratePeriod,
'groups' => $rawParams,
'global' => $isGlobalFilter
];
return $consFactory->newThrottle( $baseConsParams, $throttleParams );
case 'warn':
return $consFactory->newWarn( $baseConsParams, $rawParams[0] ?? 'abusefilter-warning' );
case 'disallow':
return $consFactory->newDisallow( $baseConsParams, $rawParams[0] ?? 'abusefilter-disallowed' );
case 'rangeblock':
return $consFactory->newRangeBlock( $baseConsParams, '1 week' );
case 'degroup':
return $consFactory->newDegroup( $baseConsParams, $this->vars );
case 'blockautopromote':
$duration = $wgAbuseFilterBlockAutopromoteDuration * 86400;
return $consFactory->newBlockAutopromote( $baseConsParams, $duration );
case 'block':
return $consFactory->newBlock( $baseConsParams, $rawParams['expiry'], $rawParams['blocktalk'] );
case 'tag':
$accountName = $this->vars->getVar( 'accountname', AbuseFilterVariableHolder::GET_BC )->toNative();
return $consFactory->newTag( $baseConsParams, $accountName, $rawParams );
default:
$registry = AbuseFilterServices::getConsequencesRegistry();
if ( array_key_exists( $actionName, $registry->getCustomActions() ) ) {
$callback = $registry->getCustomActions()[$actionName];
return $callback( $baseConsParams, $rawParams );
} elseif ( isset( $wgAbuseFilterCustomActionsHandlers[$actionName] ) ) {
wfDeprecated(
'$wgAbuseFilterCustomActionsHandlers; use the AbuseFilterCustomActions hook instead',
'1.36'
);
$customFunction = $wgAbuseFilterCustomActionsHandlers[$actionName];
return new BCConsequence( $baseConsParams, $rawParams, $this->vars, $customFunction );
} else {
$logger = LoggerFactory::getInstance( 'AbuseFilter' );
$logger->warning( "Unrecognised action $actionName" );
return null;
}
}
}
/**
* 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, function ( $el ) {
return $el instanceof ConsequencesDisablerConsequence;
} );
'@phan-var ConsequencesDisablerConsequence[] $consequenceDisablers';
uasort(
$consequenceDisablers,
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
* containing the message key followed by any message parameters.
* @todo Improve return value
*/
protected function takeConsequenceAction( Consequence $consequence ) : array {
// Special case
if ( $consequence instanceof BCConsequence ) {
$consequence->execute();
try {
$message = $consequence->getMessage();
} catch ( LogicException $_ ) {
// Swallow. Sigh.
$message = null;
}
return [ true, $message ];
}
$res = $consequence->execute();
if ( $res && $consequence instanceof HookAborterConsequence ) {
$message = $consequence->getMessage();
}
return [ $res, $message ?? null ];
}
/**
* Constructs a Status object as returned by executeFilterActions() from the list of
* actions taken and the corresponding list of messages.
*
* @param array[] $actionsTaken associative array mapping each filter to the list if
* actions taken because of that filter.
* @param array[] $messages a list of arrays, where each array contains a message key
* followed by any message parameters.
*
* @return Status
*/
protected function buildStatus( array $actionsTaken, array $messages ) : Status {
$status = Status::newGood( $actionsTaken );
foreach ( $messages as $msg ) {
$status->fatal( ...$msg );
}
return $status;
}
/**
* @return array
*/

View file

@ -172,4 +172,11 @@ class AbuseFilterServices {
public static function getVariablesBlobStore() : VariablesBlobStore {
return MediaWikiServices::getInstance()->getService( VariablesBlobStore::SERVICE_NAME );
}
/**
* @return ConsequencesExecutorFactory
*/
public static function getConsequencesExecutorFactory() : ConsequencesExecutorFactory {
return MediaWikiServices::getInstance()->getService( ConsequencesExecutorFactory::SERVICE_NAME );
}
}

View file

@ -0,0 +1,360 @@
<?php
namespace MediaWiki\Extension\AbuseFilter;
use AbuseFilterVariableHolder;
use MediaWiki\Block\BlockUser;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\Consequence\BCConsequence;
use MediaWiki\Extension\AbuseFilter\Consequence\Consequence;
use MediaWiki\Extension\AbuseFilter\Consequence\ConsequencesDisablerConsequence;
use MediaWiki\Extension\AbuseFilter\Consequence\HookAborterConsequence;
use MediaWiki\Extension\AbuseFilter\Consequence\Parameters;
use Psr\Log\LoggerInterface;
use Status;
use Title;
use User;
class ConsequencesExecutor {
public const CONSTRUCTOR_OPTIONS = [
'AbuseFilterLocallyDisabledGlobalActions',
'AbuseFilterBlockDuration',
'AbuseFilterAnonBlockDuration',
'AbuseFilterBlockAutopromoteDuration',
'AbuseFilterCustomActionsHandlers',
];
/** @var ConsequencesLookup */
private $consLookup;
/** @var ConsequencesFactory */
private $consFactory;
/** @var ConsequencesRegistry */
private $consRegistry;
/** @var FilterLookup */
private $filterLookup;
/** @var LoggerInterface */
private $logger;
/** @var ServiceOptions */
private $options;
/** @var User */
private $user;
/** @var Title */
private $title;
/** @var AbuseFilterVariableHolder */
private $vars;
/**
* @param ConsequencesLookup $consLookup
* @param ConsequencesFactory $consFactory
* @param ConsequencesRegistry $consRegistry
* @param FilterLookup $filterLookup
* @param LoggerInterface $logger
* @param ServiceOptions $options
* @param User $user
* @param Title $title
* @param AbuseFilterVariableHolder $vars
*/
public function __construct(
ConsequencesLookup $consLookup,
ConsequencesFactory $consFactory,
ConsequencesRegistry $consRegistry,
FilterLookup $filterLookup,
LoggerInterface $logger,
ServiceOptions $options,
User $user,
Title $title,
AbuseFilterVariableHolder $vars
) {
$this->consLookup = $consLookup;
$this->consFactory = $consFactory;
$this->consRegistry = $consRegistry;
$this->filterLookup = $filterLookup;
$this->logger = $logger;
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
$this->user = $user;
$this->title = $title;
$this->vars = $vars;
}
/**
* Executes a set of actions.
*
* @param string[] $filters
* @return Status returns the operation's status. $status->isOK() will return true if
* there were no actions taken, false otherwise. $status->getValue() will return
* an array listing the actions taken. $status->getErrors() etc. will provide
* 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 );
$actionsTaken = array_fill_keys( $filters, [] );
$messages = [];
foreach ( $actionsToTake as $filter => $actions ) {
foreach ( $actions as $action => $info ) {
[ $executed, $newMsg ] = $this->takeConsequenceAction( $info );
if ( $newMsg !== null ) {
$messages[] = $newMsg;
}
if ( $executed ) {
$actionsTaken[$filter][] = $action;
}
}
}
return $this->buildStatus( $actionsTaken, $messages );
}
/**
* 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
* @return Consequence[][]
* @internal Temporarily public
*/
public function replaceArraysWithConsequences( array $actionsByFilter ) : array {
// Keep track of the longest block
$maxBlock = [ 'id' => null, 'expiry' => -1, 'blocktalk' => null ];
$dangerousActions = $this->consRegistry->getDangerousActionNames();
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'] );
}
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' );
}
}
$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] );
}
}
}
}
unset( $actions );
if ( $maxBlock['id'] !== null ) {
$id = $maxBlock['id'];
unset( $maxBlock['id'] );
$actionsByFilter[$id]['block'] = $this->actionsParamsToConsequence( 'block', $maxBlock, $id );
}
return $actionsByFilter;
}
/**
* @param string $actionName
* @param array $rawParams
* @param int|string $filter
* @return Consequence|null
*/
private function actionsParamsToConsequence( string $actionName, array $rawParams, $filter ) : ?Consequence {
[ $filterID, $isGlobalFilter ] = GlobalNameUtils::splitGlobalName( $filter );
$filterObj = $this->filterLookup->getFilter( $filterID, $isGlobalFilter );
$baseConsParams = new Parameters(
$filterObj,
$isGlobalFilter,
$this->user,
$this->title,
$this->vars->getVar( 'action' )->toString()
);
switch ( $actionName ) {
case 'throttle':
$throttleId = array_shift( $rawParams );
list( $rateCount, $ratePeriod ) = explode( ',', array_shift( $rawParams ) );
$throttleParams = [
'id' => $throttleId,
'count' => (int)$rateCount,
'period' => (int)$ratePeriod,
'groups' => $rawParams,
'global' => $isGlobalFilter
];
return $this->consFactory->newThrottle( $baseConsParams, $throttleParams );
case 'warn':
return $this->consFactory->newWarn( $baseConsParams, $rawParams[0] ?? 'abusefilter-warning' );
case 'disallow':
return $this->consFactory->newDisallow( $baseConsParams, $rawParams[0] ?? 'abusefilter-disallowed' );
case 'rangeblock':
return $this->consFactory->newRangeBlock( $baseConsParams, '1 week' );
case 'degroup':
return $this->consFactory->newDegroup( $baseConsParams, $this->vars );
case 'blockautopromote':
$duration = $this->options->get( 'AbuseFilterBlockAutopromoteDuration' ) * 86400;
return $this->consFactory->newBlockAutopromote( $baseConsParams, $duration );
case 'block':
return $this->consFactory->newBlock( $baseConsParams, $rawParams['expiry'], $rawParams['blocktalk'] );
case 'tag':
$accountName = $this->vars->getVar( 'accountname', AbuseFilterVariableHolder::GET_BC )->toNative();
return $this->consFactory->newTag( $baseConsParams, $accountName, $rawParams );
default:
$customHandlers = $this->options->get( 'AbuseFilterCustomActionsHandlers' );
if ( array_key_exists( $actionName, $this->consRegistry->getCustomActions() ) ) {
$callback = $this->consRegistry->getCustomActions()[$actionName];
return $callback( $baseConsParams, $rawParams );
} elseif ( isset( $customHandlers[$actionName] ) ) {
wfDeprecated(
'$wgAbuseFilterCustomActionsHandlers; use the AbuseFilterCustomActions hook instead',
'1.36'
);
$customFunction = $customHandlers[$actionName];
return new BCConsequence( $baseConsParams, $rawParams, $this->vars, $customFunction );
} else {
$this->logger->warning( "Unrecognised action $actionName" );
return null;
}
}
}
/**
* 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, function ( $el ) {
return $el instanceof ConsequencesDisablerConsequence;
} );
'@phan-var ConsequencesDisablerConsequence[] $consequenceDisablers';
uasort(
$consequenceDisablers,
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
* containing the message key followed by any message parameters.
* @todo Improve return value
*/
private function takeConsequenceAction( Consequence $consequence ) : array {
// Special case
if ( $consequence instanceof BCConsequence ) {
$consequence->execute();
try {
$message = $consequence->getMessage();
} catch ( \LogicException $_ ) {
// Swallow. Sigh.
$message = null;
}
return [ true, $message ];
}
$res = $consequence->execute();
if ( $res && $consequence instanceof HookAborterConsequence ) {
$message = $consequence->getMessage();
}
return [ $res, $message ?? null ];
}
/**
* Constructs a Status object as returned by executeFilterActions() from the list of
* actions taken and the corresponding list of messages.
*
* @param array[] $actionsTaken associative array mapping each filter to the list if
* actions taken because of that filter.
* @param array[] $messages a list of arrays, where each array contains a message key
* followed by any message parameters.
*
* @return Status
*/
private function buildStatus( array $actionsTaken, array $messages ) : Status {
$status = Status::newGood( $actionsTaken );
foreach ( $messages as $msg ) {
$status->fatal( ...$msg );
}
return $status;
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace MediaWiki\Extension\AbuseFilter;
use AbuseFilterVariableHolder;
use MediaWiki\Config\ServiceOptions;
use Psr\Log\LoggerInterface;
use Title;
use User;
class ConsequencesExecutorFactory {
public const SERVICE_NAME = 'AbuseFilterConsequencesExecutorFactory';
/** @var ConsequencesLookup */
private $consLookup;
/** @var ConsequencesFactory */
private $consFactory;
/** @var ConsequencesRegistry */
private $consRegistry;
/** @var FilterLookup */
private $filterLookup;
/** @var LoggerInterface */
private $logger;
/** @var ServiceOptions */
private $options;
/**
* @param ConsequencesLookup $consLookup
* @param ConsequencesFactory $consFactory
* @param ConsequencesRegistry $consRegistry
* @param FilterLookup $filterLookup
* @param LoggerInterface $logger
* @param ServiceOptions $options
*/
public function __construct(
ConsequencesLookup $consLookup,
ConsequencesFactory $consFactory,
ConsequencesRegistry $consRegistry,
FilterLookup $filterLookup,
LoggerInterface $logger,
ServiceOptions $options
) {
$this->consLookup = $consLookup;
$this->consFactory = $consFactory;
$this->consRegistry = $consRegistry;
$this->filterLookup = $filterLookup;
$this->logger = $logger;
$options->assertRequiredOptions( ConsequencesExecutor::CONSTRUCTOR_OPTIONS );
$this->options = $options;
}
/**
* @param User $user
* @param Title $title
* @param AbuseFilterVariableHolder $vars
* @return ConsequencesExecutor
*/
public function newExecutor( User $user, Title $title, AbuseFilterVariableHolder $vars ) : ConsequencesExecutor {
return new ConsequencesExecutor(
$this->consLookup,
$this->consFactory,
$this->consRegistry,
$this->filterLookup,
$this->logger,
$this->options,
$user,
$title,
$vars
);
}
}

View file

@ -9,6 +9,8 @@ use MediaWiki\Extension\AbuseFilter\CentralDBManager;
use MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagger;
use MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagsManager;
use MediaWiki\Extension\AbuseFilter\ChangeTags\ChangeTagValidator;
use MediaWiki\Extension\AbuseFilter\ConsequencesExecutor;
use MediaWiki\Extension\AbuseFilter\ConsequencesExecutorFactory as ConsExecutorFactory;
use MediaWiki\Extension\AbuseFilter\ConsequencesFactory;
use MediaWiki\Extension\AbuseFilter\ConsequencesLookup;
use MediaWiki\Extension\AbuseFilter\ConsequencesRegistry;
@ -222,6 +224,19 @@ return [
$services->getMainConfig()->get( 'AbuseFilterCentralDB' )
);
},
ConsExecutorFactory::SERVICE_NAME => function ( MediaWikiServices $services ) : ConsExecutorFactory {
return new ConsExecutorFactory(
$services->get( ConsequencesLookup::SERVICE_NAME ),
$services->get( ConsequencesFactory::SERVICE_NAME ),
$services->get( ConsequencesRegistry::SERVICE_NAME ),
$services->get( FilterLookup::SERVICE_NAME ),
LoggerFactory::getInstance( 'AbuseFilter' ),
new ServiceOptions(
ConsequencesExecutor::CONSTRUCTOR_OPTIONS,
$services->getMainConfig()
)
);
},
];
// @codeCoverageIgnoreEnd

View file

@ -59,6 +59,8 @@ use PHPUnit\Framework\MockObject\MockObject;
* @covers \MediaWiki\Extension\AbuseFilter\Consequence\Throttle
* @covers \MediaWiki\Extension\AbuseFilter\Consequence\Warn
* @covers \MediaWiki\Extension\AbuseFilter\ConsequencesFactory
* @covers \MediaWiki\Extension\AbuseFilter\ConsequencesLookup
* @covers \MediaWiki\Extension\AbuseFilter\ConsequencesExecutor
*/
class AbuseFilterConsequencesTest extends MediaWikiTestCase {
use AbuseFilterCreateAccountTestTrait;

View file

@ -1,12 +1,16 @@
<?php
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\AbuseFilterServices;
use MediaWiki\Extension\AbuseFilter\ConsequencesExecutor;
use MediaWiki\Extension\AbuseFilter\ConsequencesLookup;
use MediaWiki\Extension\AbuseFilter\Filter\Filter;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Revision\MutableRevisionRecord;
use MediaWiki\Revision\RevisionRecord;
use MediaWiki\Revision\SlotRecord;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\NullLogger;
/**
* Generic tests for utility functions in AbuseFilter that require DB access
@ -297,24 +301,25 @@ class AbuseFilterDBTest extends MediaWikiTestCase {
* @param array $rawConsequences A raw, unfiltered list of consequences
* @param array $expectedKeys
* @param Title $title
* @covers AbuseFilterRunner::getFilteredConsequences
* @covers AbuseFilterRunner::replaceArraysWithConsequences
* @covers \MediaWiki\Extension\AbuseFilter\ConsequencesExecutor
* @dataProvider provideConsequences
*/
public function testGetFilteredConsequences( $rawConsequences, $expectedKeys, Title $title ) {
$this->setMwGlobals( [
'wgAbuseFilterLocallyDisabledGlobalActions' => [
'flag' => false,
'throttle' => false,
'warn' => false,
'disallow' => false,
'blockautopromote' => true,
'block' => true,
'rangeblock' => true,
'degroup' => true,
'tag' => false
]
] );
$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( Filter::class );
$fakeFilter->method( 'getName' )->willReturn( 'unused name' );
$fakeLookup = $this->createMock( FilterLookup::class );
@ -322,8 +327,19 @@ class AbuseFilterDBTest extends MediaWikiTestCase {
$this->setService( FilterLookup::SERVICE_NAME, $fakeLookup );
$user = $this->getTestUser()->getUser();
$vars = AbuseFilterVariableHolder::newFromArray( [ 'action' => 'edit' ] );
$runner = new AbuseFilterRunner( $user, $title, $vars, 'default' );
$actual = $runner->getFilteredConsequences( $runner->replaceArraysWithConsequences( $rawConsequences ) );
$executor = new ConsequencesExecutor(
$this->createMock( ConsequencesLookup::class ),
AbuseFilterServices::getConsequencesFactory(),
AbuseFilterServices::getConsequencesRegistry(),
$fakeLookup,
new NullLogger,
$options,
$user,
$title,
$vars
);
$actual = $executor->getFilteredConsequences(
$executor->replaceArraysWithConsequences( $rawConsequences ) );
$actualKeys = [];
foreach ( $actual as $filter => $actions ) {