Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.53% covered (success)
98.53%
67 / 68
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Degroup
98.53% covered (success)
98.53%
67 / 68
75.00% covered (warning)
75.00%
3 / 4
13
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 execute
96.88% covered (success)
96.88%
31 / 32
0.00% covered (danger)
0.00%
0 / 1
7
 revert
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
4
 getMessage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3namespace MediaWiki\Extension\AbuseFilter\Consequences\Consequence;
4
5use ManualLogEntry;
6use MediaWiki\Extension\AbuseFilter\Consequences\Parameters;
7use MediaWiki\Extension\AbuseFilter\FilterUser;
8use MediaWiki\Extension\AbuseFilter\GlobalNameUtils;
9use MediaWiki\Extension\AbuseFilter\Variables\LazyLoadedVariable;
10use MediaWiki\Extension\AbuseFilter\Variables\UnsetVariableException;
11use MediaWiki\Extension\AbuseFilter\Variables\VariableHolder;
12use MediaWiki\Title\TitleValue;
13use MediaWiki\User\UserGroupManager;
14use MediaWiki\User\UserIdentity;
15use MediaWiki\User\UserIdentityUtils;
16use MessageLocalizer;
17
18/**
19 * Consequence that removes all user groups from a user.
20 */
21class Degroup extends Consequence implements HookAborterConsequence, ReversibleConsequence {
22    /**
23     * @var VariableHolder
24     * @todo This dependency is subpar
25     */
26    private $vars;
27
28    /** @var UserGroupManager */
29    private $userGroupManager;
30
31    /** @var UserIdentityUtils */
32    private $userIdentityUtils;
33
34    /** @var FilterUser */
35    private $filterUser;
36
37    /** @var MessageLocalizer */
38    private $messageLocalizer;
39
40    /**
41     * @param Parameters $params
42     * @param VariableHolder $vars
43     * @param UserGroupManager $userGroupManager
44     * @param UserIdentityUtils $userIdentityUtils
45     * @param FilterUser $filterUser
46     * @param MessageLocalizer $messageLocalizer
47     */
48    public function __construct(
49        Parameters $params,
50        VariableHolder $vars,
51        UserGroupManager $userGroupManager,
52        UserIdentityUtils $userIdentityUtils,
53        FilterUser $filterUser,
54        MessageLocalizer $messageLocalizer
55    ) {
56        parent::__construct( $params );
57        $this->vars = $vars;
58        $this->userGroupManager = $userGroupManager;
59        $this->userIdentityUtils = $userIdentityUtils;
60        $this->filterUser = $filterUser;
61        $this->messageLocalizer = $messageLocalizer;
62    }
63
64    /**
65     * @inheritDoc
66     */
67    public function execute(): bool {
68        $user = $this->parameters->getUser();
69
70        if ( !$this->userIdentityUtils->isNamed( $user ) ) {
71            return false;
72        }
73
74        // Pull the groups from the VariableHolder, so that they will always be computed.
75        // This allow us to pull the groups from the VariableHolder to undo the degroup
76        // via Special:AbuseFilter/revert.
77        try {
78            // No point in triggering a lazy-load, instead we compute it here if necessary
79            $groupsVar = $this->vars->getVarThrow( 'user_groups' );
80        } catch ( UnsetVariableException $_ ) {
81            $groupsVar = null;
82        }
83        if ( $groupsVar === null || $groupsVar instanceof LazyLoadedVariable ) {
84            // The variable is unset or not computed. Compute it and update the holder so we can use it for reverts
85            $groups = $this->userGroupManager->getUserEffectiveGroups( $user );
86            $this->vars->setVar( 'user_groups', $groups );
87        } else {
88            $groups = $groupsVar->toNative();
89        }
90
91        $implicitGroups = $this->userGroupManager->listAllImplicitGroups();
92        $removeGroups = array_diff( $groups, $implicitGroups );
93        if ( !count( $removeGroups ) ) {
94            return false;
95        }
96
97        foreach ( $removeGroups as $group ) {
98            $this->userGroupManager->removeUserFromGroup( $user, $group );
99        }
100
101        // TODO Core should provide a logging method
102        $logEntry = new ManualLogEntry( 'rights', 'rights' );
103        $logEntry->setPerformer( $this->filterUser->getUserIdentity() );
104        $logEntry->setTarget( new TitleValue( NS_USER, $user->getName() ) );
105        $logEntry->setComment(
106            $this->messageLocalizer->msg(
107                'abusefilter-degroupreason',
108                $this->parameters->getFilter()->getName(),
109                $this->parameters->getFilter()->getID()
110            )->inContentLanguage()->text()
111        );
112        $logEntry->setParameters( [
113            '4::oldgroups' => $removeGroups,
114            '5::newgroups' => []
115        ] );
116        $logEntry->publish( $logEntry->insert() );
117        return true;
118    }
119
120    /**
121     * @inheritDoc
122     */
123    public function revert( UserIdentity $performer, string $reason ): bool {
124        $user = $this->parameters->getUser();
125        $currentGroups = $this->userGroupManager->getUserGroups( $user );
126        // Pull the user's original groups from the vars. This is guaranteed to be set, because we
127        // enforce it when performing a degroup.
128        $removedGroups = $this->vars->getComputedVariable( 'user_groups' )->toNative();
129        $removedGroups = array_diff(
130            $removedGroups,
131            $this->userGroupManager->listAllImplicitGroups(),
132            $currentGroups
133        );
134
135        $addedGroups = [];
136        foreach ( $removedGroups as $group ) {
137            // TODO An addUserToGroups method with bulk updates would be nice
138            if ( $this->userGroupManager->addUserToGroup( $user, $group ) ) {
139                $addedGroups[] = $group;
140            }
141        }
142
143        // Don't log if no groups were added.
144        if ( !$addedGroups ) {
145            return false;
146        }
147
148        // TODO Core should provide a logging method
149        $logEntry = new ManualLogEntry( 'rights', 'rights' );
150        $logEntry->setTarget( new TitleValue( NS_USER, $user->getName() ) );
151        $logEntry->setPerformer( $performer );
152        $logEntry->setComment( $reason );
153        $logEntry->setParameters( [
154            '4::oldgroups' => $currentGroups,
155            '5::newgroups' => array_merge( $currentGroups, $addedGroups )
156        ] );
157        $logEntry->publish( $logEntry->insert() );
158
159        return true;
160    }
161
162    /**
163     * @inheritDoc
164     */
165    public function getMessage(): array {
166        $filter = $this->parameters->getFilter();
167        return [
168            'abusefilter-degrouped',
169            $filter->getName(),
170            GlobalNameUtils::buildGlobalName( $filter->getID(), $this->parameters->getIsGlobalFilter() )
171        ];
172    }
173}