Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Hooks
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 4
600
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getIPFromUser
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 onGetUserPermissionsErrorsExpensive
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
272
 onOtherBlockLogLink
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * https://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21namespace MediaWiki\Extension\StopForumSpam;
22
23use MediaWiki\Block\DatabaseBlock;
24use MediaWiki\Config\Config;
25use MediaWiki\Hook\OtherBlockLogLinkHook;
26use MediaWiki\Html\Html;
27use MediaWiki\Logger\LoggerFactory;
28use MediaWiki\Permissions\Hook\GetUserPermissionsErrorsExpensiveHook;
29use MediaWiki\Title\Title;
30use MediaWiki\User\User;
31use RequestContext;
32use Wikimedia\IPUtils;
33
34class Hooks implements
35    GetUserPermissionsErrorsExpensiveHook,
36    OtherBlockLogLinkHook
37{
38
39    /** @var Config */
40    private Config $config;
41
42    /**
43     * @param Config $config
44     */
45    public function __construct( Config $config ) {
46        $this->config = $config;
47    }
48
49    /**
50     * Get an IP address for a User if possible
51     *
52     * @param User $user
53     * @return bool|string IP address or false
54     */
55    public static function getIPFromUser( User $user ) {
56        $context = RequestContext::getMain();
57        if ( $context->getUser()->getName() === $user->getName() ) {
58            // Only use the main context if the users are the same
59            return $context->getRequest()->getIP();
60        }
61
62        // Couldn't figure out an IP address
63        return false;
64    }
65
66    /**
67     * If an IP address is deny-listed, don't let them edit.
68     *
69     * @param Title $title Title being acted upon
70     * @param User $user User performing the action
71     * @param string $action Action being performed
72     * @param array &$result Will be filled with block status if blocked
73     * @return bool
74     */
75    public function onGetUserPermissionsErrorsExpensive( $title, $user, $action, &$result ) {
76        if ( !$this->config->get( 'SFSIPListLocation' ) ) {
77            // Not configured
78            return true;
79        }
80        if ( $action === 'read' ) {
81            return true;
82        }
83        if ( $this->config->get( 'BlockAllowsUTEdit' ) && $title->equals( $user->getTalkPage() ) ) {
84            // Let a user edit their talk page
85            return true;
86        }
87
88        $exemptReasons = [];
89        $logger = LoggerFactory::getInstance( 'StopForumSpam' );
90        $ip = self::getIPFromUser( $user );
91
92        // attempt to get ip from user
93        if ( $ip === false ) {
94            $exemptReasons[] = "Unable to obtain IP information for {user}";
95        }
96
97        // allow if user has sfsblock-bypass
98        if ( $user->isAllowed( 'sfsblock-bypass' ) ) {
99            $exemptReasons[] = "{user} is exempt from SFS blocks";
100        }
101
102        // allow if user is exempted from autoblocks (borrowed from TorBlock)
103        if ( DatabaseBlock::isExemptedFromAutoblocks( $ip ) ) {
104            $exemptReasons[] = "{clientip} is in autoblock exemption list, exempting from SFS blocks";
105        }
106
107        $denyListManager = DenyListManager::singleton();
108        if ( !$this->config->get( 'SFSReportOnly' ) ) {
109            // enforce mode + ip not deny-listed = allow action
110            if ( !$denyListManager->isIpDenyListed( $ip ) || count( $exemptReasons ) > 0 ) {
111                return true;
112            }
113        } elseif (
114            $denyListManager->isIpDenyListed( $ip ) &&
115            count( $exemptReasons ) > 0
116        ) {
117            // report-only mode + ip deny-listed = allow action and log
118            $exemptReasonsStr = implode( ', ', $exemptReasons );
119            $logger->info(
120                $exemptReasonsStr,
121                [
122                    'action' => $action,
123                    'clientip' => $ip,
124                    'title' => $title->getPrefixedText(),
125                    'user' => $user->getName(),
126                    'reportonly' => $this->config->get( 'SFSReportOnly' )
127                ]
128            );
129
130            return true;
131
132        } elseif ( !$denyListManager->isIpDenyListed( $ip ) ) {
133            // report-only mode + ip NOT deny-listed
134            return true;
135        }
136
137        // log blocked action, regardless of report-only mode
138        $blockVerb = ( $this->config->get( 'SFSReportOnly' ) ) ? 'would have been' : 'was';
139        $logger->info(
140            "{user} {$blockVerb} blocked by SFS from doing {action} "
141            . "by using {clientip} on \"{title}\".",
142            [
143                'action' => $action,
144                'clientip' => $ip,
145                'title' => $title->getPrefixedText(),
146                'user' => $user->getName(),
147                'reportonly' => $this->config->get( 'SFSReportOnly' )
148            ]
149        );
150
151        // final catch-all for report-only mode
152        if ( $this->config->get( 'SFSReportOnly' ) ) {
153            return true;
154        }
155
156        // default: set error msg result and return false
157        $result = [ 'stopforumspam-blocked', $ip ];
158        return false;
159    }
160
161    /**
162     * @param array &$msg
163     * @param string $ip
164     * @return bool
165     */
166    public function onOtherBlockLogLink( &$msg, $ip ) {
167        if (
168            !$this->config->get( 'SFSIPListLocation' ) ||
169            $this->config->get( 'SFSReportOnly' )
170        ) {
171            return true;
172        }
173
174        $denyListManager = DenyListManager::singleton();
175        if ( IPUtils::isIPAddress( $ip ) && $denyListManager->isIpDenyListed( $ip ) ) {
176            $msg[] = Html::rawElement(
177                'span',
178                [ 'class' => 'mw-stopforumspam-denylisted' ],
179                wfMessage( 'stopforumspam-is-blocked', $ip )->parse()
180            );
181        }
182
183        return true;
184    }
185}