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