Page MenuHomeWickedGov Phorge

ConsequencesExecutor.php
No OneTemporary

Size
13 KB
Referenced Files
None
Subscribers
None

ConsequencesExecutor.php

<?php
namespace MediaWiki\Extension\AbuseFilter\Consequences;
use MediaWiki\Block\BlockUser;
use MediaWiki\Config\ServiceOptions;
use MediaWiki\Extension\AbuseFilter\ActionSpecifier;
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;
use MediaWiki\Extension\AbuseFilter\FilterLookup;
use MediaWiki\Extension\AbuseFilter\GlobalNameUtils;
use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
use MediaWiki\Message\Message;
use MediaWiki\Status\Status;
use MediaWiki\User\UserIdentityUtils;
use Psr\Log\LoggerInterface;
class ConsequencesExecutor {
public const CONSTRUCTOR_OPTIONS = [
'AbuseFilterLocallyDisabledGlobalActions',
'AbuseFilterBlockDuration',
'AbuseFilterAnonBlockDuration',
'AbuseFilterBlockAutopromoteDuration',
];
/** @var ConsequencesLookup */
private $consLookup;
/** @var ConsequencesFactory */
private $consFactory;
/** @var ConsequencesRegistry */
private $consRegistry;
/** @var FilterLookup */
private $filterLookup;
/** @var LoggerInterface */
private $logger;
/** @var UserIdentityUtils */
private $userIdentityUtils;
/** @var ServiceOptions */
private $options;
/** @var ActionSpecifier */
private $specifier;
/** @var VariableHolder */
private $vars;
/**
* @param ConsequencesLookup $consLookup
* @param ConsequencesFactory $consFactory
* @param ConsequencesRegistry $consRegistry
* @param FilterLookup $filterLookup
* @param LoggerInterface $logger
* @param UserIdentityUtils $userIdentityUtils
* @param ServiceOptions $options
* @param ActionSpecifier $specifier
* @param VariableHolder $vars
*/
public function __construct(
ConsequencesLookup $consLookup,
ConsequencesFactory $consFactory,
ConsequencesRegistry $consRegistry,
FilterLookup $filterLookup,
LoggerInterface $logger,
UserIdentityUtils $userIdentityUtils,
ServiceOptions $options,
ActionSpecifier $specifier,
VariableHolder $vars
) {
$this->consLookup = $consLookup;
$this->consFactory = $consFactory;
$this->consRegistry = $consRegistry;
$this->filterLookup = $filterLookup;
$this->logger = $logger;
$this->userIdentityUtils = $userIdentityUtils;
$options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
$this->options = $options;
$this->specifier = $specifier;
$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->getMessages() will provide
* the errors and warnings to be shown to the user to explain the actions.
*/
public function executeFilterActions( array $filters ): Status {
$actionsToTake = $this->getActualConsequencesToExecute( $filters );
$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 );
}
/**
* @param string[] $filters
* @return Consequence[][]
* @internal
*/
public function getActualConsequencesToExecute( array $filters ): array {
$rawConsParamsByFilter = $this->consLookup->getConsequencesForFilters( $filters );
$consParamsByFilter = $this->replaceLegacyParameters( $rawConsParamsByFilter );
$specializedConsParams = $this->specializeParameters( $consParamsByFilter );
$allowedConsParams = $this->removeForbiddenConsequences( $specializedConsParams );
$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 ) {
if ( $name === 'block' && count( $parameters ) !== 3 ) {
// Old type with fixed expiry
$blockTalk = in_array( 'blocktalk', $parameters, true );
$consParams[$filter][$name] = [
$blockTalk ? 'blocktalk' : 'noTalkBlockSet',
$anonBlockDuration,
$registeredBlockDuration
];
}
}
}
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 {
$user = $this->specifier->getUser();
$isNamed = $this->userIdentityUtils->isNamed( $user );
foreach ( $consParams as $filter => $actions ) {
foreach ( $actions as $name => $parameters ) {
if ( $name === 'block' ) {
$consParams[$filter][$name] = [
'expiry' => $isNamed ? $parameters[2] : $parameters[1],
'blocktalk' => $parameters[0] === 'blocktalk'
];
}
}
}
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;
}
/**
* @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->specifier
);
switch ( $actionName ) {
case 'throttle':
$throttleId = array_shift( $rawParams );
[ $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':
return $this->consFactory->newTag( $baseConsParams, $rawParams );
default:
if ( array_key_exists( $actionName, $this->consRegistry->getCustomActions() ) ) {
$callback = $this->consRegistry->getCustomActions()[$actionName];
return $callback( $baseConsParams, $rawParams );
} else {
$this->logger->warning( "Unrecognised action $actionName" );
return null;
}
}
}
/**
* @param Consequence $consequence
* @return array [ executed (bool), message (?Message) ]
* @phan-return array{0:bool, 1:?Message}
*/
private function takeConsequenceAction( Consequence $consequence ): array {
$res = $consequence->execute();
if ( $res && $consequence instanceof HookAborterConsequence ) {
$message = Message::newFromSpecifier( $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 Message[] $messages a list of Message objects
*
* @return Status
*/
private function buildStatus( array $actionsTaken, array $messages ): Status {
$status = Status::newGood( $actionsTaken );
foreach ( $messages as $msg ) {
$status->fatal( $msg );
}
return $status;
}
}

File Metadata

Mime Type
text/x-php
Expires
Tue, Aug 18, 22:08 (1 d, 17 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
39/17/39d76559a70984b48b36b9450911
Default Alt Text
ConsequencesExecutor.php (13 KB)

Event Timeline