mirror of
https://gerrit.wikimedia.org/r/mediawiki/extensions/AbuseFilter.git
synced 2024-11-23 21:53:35 +00:00
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:
parent
f430cd211a
commit
68adaa5cb1
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
|
360
includes/ConsequencesExecutor.php
Normal file
360
includes/ConsequencesExecutor.php
Normal 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;
|
||||
}
|
||||
}
|
71
includes/ConsequencesExecutorFactory.php
Normal file
71
includes/ConsequencesExecutorFactory.php
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
Loading…
Reference in a new issue