Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 13
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 81
0.00% covered (danger)
0.00%
0 / 13
1406
0.00% covered (danger)
0.00%
0 / 1
 registerExtension
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkUserCan
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 onGetUserPermissionsErrorsExpensive
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 onUserCanSendEmail
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 onGetUserBlock
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 onAbortAutoblock
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onGetAutoPromoteGroups
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 onAutopromoteCondition
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 onRecentChange_Save
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 onListDefinedTags
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onChangeTagsListActive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onOtherBlockLogLink
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * Hooks for the Extension:TorBlock for MediaWiki
5 *
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with this program; if not, write to the Free Software Foundation, Inc.,
18 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 * http://www.gnu.org/copyleft/gpl.html
20 *
21 * @file
22 * @ingroup Extensions
23 * @link https://www.mediawiki.org/wiki/Extension:TorBlock Documentation
24 *
25 * @author Andrew Garrett <andrew@epstone.net>
26 * @license GPL-2.0-or-later
27 */
28
29// phpcs:disable MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName
30// Need to be able to define ::onRecentChange_Save
31
32namespace MediaWiki\Extension\TorBlock;
33
34use MediaWiki\Block\AbstractBlock;
35use MediaWiki\Block\DatabaseBlock;
36use MediaWiki\Block\Hook\AbortAutoblockHook;
37use MediaWiki\Block\Hook\GetUserBlockHook;
38use MediaWiki\ChangeTags\Hook\ChangeTagsListActiveHook;
39use MediaWiki\ChangeTags\Hook\ListDefinedTagsHook;
40use MediaWiki\Extension\TorBlock\Hooks\HookRunner;
41use MediaWiki\Hook\OtherBlockLogLinkHook;
42use MediaWiki\Hook\RecentChange_saveHook;
43use MediaWiki\HookContainer\HookContainer;
44use MediaWiki\Html\Html;
45use MediaWiki\MediaWikiServices;
46use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsExpensiveHook;
47use MediaWiki\RecentChanges\RecentChange;
48use MediaWiki\Title\Title;
49use MediaWiki\User\Hook\AutopromoteConditionHook;
50use MediaWiki\User\Hook\GetAutoPromoteGroupsHook;
51use MediaWiki\User\Hook\UserCanSendEmailHook;
52use MediaWiki\User\User;
53use Wikimedia\IPUtils;
54
55class Hooks implements
56    AbortAutoblockHook,
57    AutopromoteConditionHook,
58    GetUserPermissionsErrorsExpensiveHook,
59    GetAutoPromoteGroupsHook,
60    GetUserBlockHook,
61    RecentChange_saveHook,
62    ListDefinedTagsHook,
63    ChangeTagsListActiveHook,
64    UserCanSendEmailHook,
65    OtherBlockLogLinkHook
66{
67
68    private HookRunner $hookRunner;
69
70    public static function registerExtension() {
71        // Define new autopromote condition
72        // Numbers won't work, we'll get collisions
73        define( 'APCOND_TOR', 'tor' );
74    }
75
76    public function __construct( HookContainer $hookContainer ) {
77        $this->hookRunner = new HookRunner( $hookContainer );
78    }
79
80    /**
81     * Whether the given user is allowed to perform $action from its current IP
82     *
83     * @param User $user
84     * @param string|null $action
85     * @return bool
86     */
87    private static function checkUserCan( User $user, $action = null ) {
88        global $wgTorAllowedActions, $wgRequest;
89
90        if ( ( $action !== null && in_array( $action, $wgTorAllowedActions ) )
91            || !TorExitNodes::isExitNode()
92        ) {
93            return true;
94        }
95
96        wfDebugLog( 'torblock', "User detected as editing through tor." );
97
98        global $wgTorBypassPermissions;
99        foreach ( $wgTorBypassPermissions as $perm ) {
100            if ( $user->isAllowed( $perm ) ) {
101                wfDebugLog( 'torblock', "User has $perm permission. Exempting from Tor Blocks." );
102
103                return true;
104            }
105        }
106
107        $ip = $wgRequest->getIP();
108
109        if ( MediaWikiServices::getInstance()->getAutoblockExemptionList()->isExempt( $ip ) ) {
110            wfDebugLog( 'torblock', "IP is excluded from autoblocks. Exempting from Tor Blocks." );
111
112            return true;
113        }
114
115        return false;
116    }
117
118    /**
119     * Check if a user is a Tor node and not excluded from autoblocks or allowed
120     * to bypass tor blocks.
121     *
122     * @param Title $title Title being acted upon
123     * @param User $user User performing the action
124     * @param string $action Action being performed
125     * @param array &$result Will be filled with block status if blocked
126     * @return bool
127     */
128    public function onGetUserPermissionsErrorsExpensive(
129        $title,
130        $user,
131        $action,
132        &$result
133    ) {
134        global $wgRequest;
135        if ( !self::checkUserCan( $user, $action ) ) {
136            wfDebugLog( 'torblock', "User detected as editing from Tor node. " .
137                "Adding Tor block to permissions errors." );
138
139            // Allow site customization of blocked message.
140            $blockedMsg = 'torblock-blocked';
141            $this->hookRunner->onTorBlockBlockedMsg( $blockedMsg );
142            $result = [ $blockedMsg, $wgRequest->getIP() ];
143
144            return false;
145        }
146
147        return true;
148    }
149
150    /**
151     * Check if the user is logged in from a Tor exit node but is not exempt.
152     * If so, block the user.
153     *
154     * @param User $user
155     * @param array &$hookErr
156     * @return bool
157     */
158    public function onUserCanSendEmail( $user, &$hookErr ) {
159        global $wgRequest;
160        if ( !self::checkUserCan( $user ) ) {
161            wfDebugLog( 'torblock', "User detected as trying to send an email from Tor node. Preventing." );
162
163            // Allow site customization of blocked message.
164            $blockedMsg = 'torblock-blocked';
165            $this->hookRunner->onTorBlockBlockedMsg( $blockedMsg );
166            $hookErr = [
167                'permissionserrors',
168                $blockedMsg,
169                [ $wgRequest->getIP() ],
170            ];
171            return false;
172        }
173
174        return true;
175    }
176
177    /**
178     * Remove a block if it only targets a Tor node. A composite block comprises
179     * multiple blocks, and if any of these target the user, then do not remove the
180     * block.
181     *
182     * @param User $user
183     * @param string|null $ip
184     * @param AbstractBlock|null &$block
185     * @return bool
186     */
187    public function onGetUserBlock( $user, $ip, &$block ) {
188        global $wgTorDisableAdminBlocks;
189        if ( !$block || !$wgTorDisableAdminBlocks || !TorExitNodes::isExitNode() ) {
190            return true;
191        }
192
193        $blocks = $block->toArray();
194
195        $removeBlock = true;
196        foreach ( $blocks as $singleBlock ) {
197            if ( $singleBlock->getType() === AbstractBlock::TYPE_USER ) {
198                $removeBlock = false;
199                break;
200            }
201        }
202
203        if ( $removeBlock ) {
204            wfDebugLog( 'torblock', "User using Tor node. Disabling IP block as it was " .
205                "probably targeted at the Tor node." );
206            // Node is probably blocked for being a Tor node. Remove block.
207            $block = null;
208        }
209
210        return true;
211    }
212
213    /**
214     * If an IP address is an exit node, stop it from being autoblocked.
215     *
216     * @param string $autoblockip IP address being blocked
217     * @param DatabaseBlock $block Block being applied
218     * @return bool
219     */
220    public function onAbortAutoblock( $autoblockip, $block ) {
221        return !TorExitNodes::isExitNode( $autoblockip );
222    }
223
224    /**
225     * When the user is a Tor exit node, make sure they meet configured
226     * age/edit count requirements before allowing promotions.
227     *
228     * @param User $user User being promoted
229     * @param array &$promote Groups being added
230     * @return bool
231     */
232    public function onGetAutoPromoteGroups( $user, &$promote ) {
233        global $wgTorAutoConfirmAge, $wgTorAutoConfirmCount;
234
235        // Check against stricter requirements for tor nodes.
236        // Counterintuitively, we do the requirement checks first.
237        // This is so that we don't have to hit memcached to get the
238        // exit list, unnecessarily.
239
240        if ( !count( $promote ) ) {
241            // No groups to promote to anyway
242            return true;
243        }
244
245        $age = time() - (int)wfTimestampOrNull( TS_UNIX, $user->getRegistration() );
246
247        if ( $age >= $wgTorAutoConfirmAge && $user->getEditCount() >= $wgTorAutoConfirmCount ) {
248            // Does match requirements. Don't bother checking if we're an exit node.
249            return true;
250        }
251
252        if ( TorExitNodes::isExitNode() ) {
253            // Tor user, doesn't match the expanded requirements.
254            $promote = [];
255        }
256
257        return true;
258    }
259
260    /**
261     * Check if a user is a Tor node if the wiki is configured
262     * to autopromote on Tor status.
263     *
264     * @param string $type Condition being checked
265     * @param array $args Arguments passed to the condition
266     * @param User $user User being promoted
267     * @param null|bool &$result Will be filled with result of condition
268     * @return bool
269     */
270    public function onAutopromoteCondition( $type, $args, $user, &$result ) {
271        if ( $type == APCOND_TOR ) {
272            $result = TorExitNodes::isExitNode();
273        }
274
275        return true;
276    }
277
278    /**
279     * If enabled, tag recent changes made by a Tor exit node.
280     *
281     * @param RecentChange $recentChange The change being saved
282     * @return bool true
283     */
284    public function onRecentChange_Save( $recentChange ) {
285        global $wgTorTagChanges;
286
287        if ( $wgTorTagChanges && TorExitNodes::isExitNode() ) {
288            $recentChange->addTags( 'tor' );
289        }
290        return true;
291    }
292
293    /**
294     * If enabled, add a new tag type for recent changes made by Tor exit nodes.
295     *
296     * @param array &$emptyTags List of defined tags (for ListDefinedTags hook) or
297     * list of active tags (for ChangeTagsListActive hook)
298     * @return bool true
299     */
300    public function onListDefinedTags( &$emptyTags ) {
301        global $wgTorTagChanges;
302
303        if ( $wgTorTagChanges ) {
304            $emptyTags[] = 'tor';
305        }
306        return true;
307    }
308
309    /**
310     * @param string[] &$tags
311     *
312     * @return bool
313     */
314    public function onChangeTagsListActive( &$tags ) {
315        return $this->onListDefinedTags( $tags );
316    }
317
318    /**
319     * Creates a message with the Tor blocking status if applicable.
320     *
321     * @param array &$msg Message with the status
322     * @param string $ip The IP address to be checked
323     * @return bool true
324     */
325    public function onOtherBlockLogLink( &$msg, $ip ) {
326        // IP addresses can be blocked only
327        // Fast return if IP is not an exit node
328        if ( IPUtils::isIPAddress( $ip ) && TorExitNodes::isExitNode( $ip ) ) {
329            $msg[] = Html::rawElement(
330                'span',
331                [ 'class' => 'mw-torblock-isexitnode' ],
332                wfMessage( 'torblock-isexitnode', $ip )->parse()
333            );
334        }
335        return true;
336    }
337}