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\Permissions\Hook\GetUserPermissionsErrorsExpensiveHook;
46use MediaWiki\Title\Title;
47use MediaWiki\User\Hook\AutopromoteConditionHook;
48use MediaWiki\User\Hook\GetAutoPromoteGroupsHook;
49use MediaWiki\User\Hook\UserCanSendEmailHook;
50use MediaWiki\User\User;
51use RecentChange;
52use Wikimedia\IPUtils;
53
54class Hooks implements
55    AbortAutoblockHook,
56    AutopromoteConditionHook,
57    GetUserPermissionsErrorsExpensiveHook,
58    GetAutoPromoteGroupsHook,
59    GetUserBlockHook,
60    RecentChange_saveHook,
61    ListDefinedTagsHook,
62    ChangeTagsListActiveHook,
63    UserCanSendEmailHook,
64    OtherBlockLogLinkHook
65{
66
67    private HookRunner $hookRunner;
68
69    public static function registerExtension() {
70        // Define new autopromote condition
71        // Numbers won't work, we'll get collisions
72        define( 'APCOND_TOR', 'tor' );
73    }
74
75    public function __construct( HookContainer $hookContainer ) {
76        $this->hookRunner = new HookRunner( $hookContainer );
77    }
78
79    /**
80     * Whether the given user is allowed to perform $action from its current IP
81     *
82     * @param User $user
83     * @param string|null $action
84     * @return bool
85     */
86    private static function checkUserCan( User $user, $action = null ) {
87        global $wgTorAllowedActions, $wgRequest;
88
89        if ( ( $action !== null && in_array( $action, $wgTorAllowedActions ) )
90            || !TorExitNodes::isExitNode()
91        ) {
92            return true;
93        }
94
95        wfDebugLog( 'torblock', "User detected as editing through tor." );
96
97        global $wgTorBypassPermissions;
98        foreach ( $wgTorBypassPermissions as $perm ) {
99            if ( $user->isAllowed( $perm ) ) {
100                wfDebugLog( 'torblock', "User has $perm permission. Exempting from Tor Blocks." );
101
102                return true;
103            }
104        }
105
106        $ip = $wgRequest->getIP();
107        if ( DatabaseBlock::isExemptedFromAutoblocks( $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     * Check if a user is a Tor node if the wiki is configured
260     * to autopromote on Tor status.
261     *
262     * @param string $type Condition being checked
263     * @param array $args Arguments passed to the condition
264     * @param User $user User being promoted
265     * @param array &$result Will be filled with result of condition
266     * @return bool
267     */
268    public function onAutopromoteCondition( $type, $args, $user, &$result ) {
269        if ( $type == APCOND_TOR ) {
270            $result = TorExitNodes::isExitNode();
271        }
272
273        return true;
274    }
275
276    /**
277     * If enabled, tag recent changes made by a Tor exit node.
278     *
279     * @param RecentChange $recentChange The change being saved
280     * @return bool true
281     */
282    public function onRecentChange_Save( $recentChange ) {
283        global $wgTorTagChanges;
284
285        if ( $wgTorTagChanges && TorExitNodes::isExitNode() ) {
286            $recentChange->addTags( 'tor' );
287        }
288        return true;
289    }
290
291    /**
292     * If enabled, add a new tag type for recent changes made by Tor exit nodes.
293     *
294     * @param array &$emptyTags List of defined tags (for ListDefinedTags hook) or
295     * list of active tags (for ChangeTagsListActive hook)
296     * @return bool true
297     */
298    public function onListDefinedTags( &$emptyTags ) {
299        global $wgTorTagChanges;
300
301        if ( $wgTorTagChanges ) {
302            $emptyTags[] = 'tor';
303        }
304        return true;
305    }
306
307    /**
308     * @param string[] &$tags
309     *
310     * @return bool
311     */
312    public function onChangeTagsListActive( &$tags ) {
313        return $this->onListDefinedTags( $tags );
314    }
315
316    /**
317     * Creates a message with the Tor blocking status if applicable.
318     *
319     * @param array &$msg Message with the status
320     * @param string $ip The IP address to be checked
321     * @return bool true
322     */
323    public function onOtherBlockLogLink( &$msg, $ip ) {
324        // IP addresses can be blocked only
325        // Fast return if IP is not an exit node
326        if ( IPUtils::isIPAddress( $ip ) && TorExitNodes::isExitNode( $ip ) ) {
327            $msg[] = Html::rawElement(
328                'span',
329                [ 'class' => 'mw-torblock-isexitnode' ],
330                wfMessage( 'torblock-isexitnode', $ip )->parse()
331            );
332        }
333        return true;
334    }
335}