Failed Conditions
Pull Request — bugsquish (#573)
by Simon
11:01 queued 08:44
created

PageBan   F

Complexity

Total Complexity 60

Size/Duplication

Total Lines 375
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 60
eloc 180
dl 0
loc 375
ccs 0
cts 239
cp 0
rs 3.6
c 3
b 1
f 0

11 Methods

Rating   Name   Duplication   Size   Complexity  
A main() 0 9 1
A remove() 0 42 5
A getBanDuration() 0 22 5
A show() 0 18 2
A set() 0 16 3
B validateIpBan() 0 19 9
D handlePostMethodForSetBan() 0 85 16
A getBanForUnban() 0 16 4
A setupSecurity() 0 6 1
C handleGetMethodForSetBan() 0 51 13
A setupBanList() 0 20 1

How to fix   Complexity   

Complex Class

Complex classes like PageBan often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PageBan, and based on these observations, apply Extract Interface, too.

1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 *                                                                            *
5
 * All code in this file is released into the public domain by the ACC        *
6
 * Development Team. Please see team.json for a list of contributors.         *
7
 ******************************************************************************/
8
9
namespace Waca\Pages;
10
11
use Exception;
12
use SmartyException;
13
use Waca\DataObjects\Ban;
14
use Waca\DataObjects\Request;
15
use Waca\DataObjects\User;
16
use Waca\Exceptions\AccessDeniedException;
17
use Waca\Exceptions\ApplicationLogicException;
18
use Waca\Helpers\BanHelper;
19
use Waca\Helpers\Logger;
20
use Waca\Helpers\SearchHelpers\UserSearchHelper;
21
use Waca\SessionAlert;
22
use Waca\Tasks\InternalPageBase;
23
use Waca\WebRequest;
24
25
class PageBan extends InternalPageBase
26
{
27
    /**
28
     * Main function for this page, when no specific actions are called.
29
     */
30
    protected function main()
31
    {
32
        $this->assignCSRFToken();
33
        $this->setHtmlTitle('Bans');
34
35
        $bans = Ban::getActiveBans($this->getDatabase());
36
37
        $this->setupBanList($bans);
38
        $this->setTemplate('bans/main.tpl');
39
    }
40
41
    protected function show()
42
    {
43
        $this->assignCSRFToken();
44
        $this->setHtmlTitle('Bans');
45
46
        $rawIdList = WebRequest::getString('id');
47
        if ($rawIdList === null) {
48
            $this->redirect('bans');
49
50
            return;
51
        }
52
53
        $idList = explode(',',$rawIdList);
54
55
        $bans = Ban::getByIdList($idList, $this->getDatabase());
56
57
        $this->setupBanList($bans);
58
        $this->setTemplate('bans/showbans.tpl');
59
    }
60
61
    /**
62
     * Entry point for the ban set action
63
     * @throws SmartyException
64
     * @throws Exception
65
     */
66
    protected function set()
67
    {
68
        $this->setHtmlTitle('Bans');
69
70
        // dual-mode action
71
        if (WebRequest::wasPosted()) {
72
            try {
73
                $this->handlePostMethodForSetBan();
74
            }
75
            catch (ApplicationLogicException $ex) {
76
                SessionAlert::error($ex->getMessage());
77
                $this->redirect("bans", "set");
78
            }
79
        }
80
        else {
81
            $this->handleGetMethodForSetBan();
82
        }
83
    }
84
85
    /**
86
     * Entry point for the ban remove action
87
     *
88
     * @throws AccessDeniedException
89
     * @throws ApplicationLogicException
90
     * @throws SmartyException
91
     */
92
    protected function remove()
93
    {
94
        $this->setHtmlTitle('Bans');
95
96
        $ban = $this->getBanForUnban();
97
98
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
99
        if (!$banHelper->canUnban($ban)) {
100
            // triggered when a user tries to unban a ban they can't see the entirety of.
101
            // there's no UI way to get to this, so a raw exception is fine.
102
            throw new AccessDeniedException($this->getSecurityManager());
103
        }
104
105
        // dual mode
106
        if (WebRequest::wasPosted()) {
107
            $this->validateCSRFToken();
108
            $unbanReason = WebRequest::postString('unbanreason');
109
110
            if ($unbanReason === null || trim($unbanReason) === "") {
111
                SessionAlert::error('No unban reason specified');
112
                $this->redirect("bans", "remove", array('id' => $ban->getId()));
113
            }
114
115
            // set optimistic locking from delete form page load
116
            $updateVersion = WebRequest::postInt('updateversion');
117
            $ban->setUpdateVersion($updateVersion);
118
119
            $database = $this->getDatabase();
120
            $ban->setActive(false);
121
            $ban->save();
122
123
            Logger::unbanned($database, $ban, $unbanReason);
124
125
            SessionAlert::quick('Disabled ban.');
126
            $this->getNotificationHelper()->unbanned($ban, $unbanReason);
127
128
            $this->redirect('bans');
129
        }
130
        else {
131
            $this->assignCSRFToken();
132
            $this->assign('ban', $ban);
133
            $this->setTemplate('bans/unban.tpl');
134
        }
135
    }
136
137
    /**
138
     * @throws ApplicationLogicException
139
     */
140
    private function getBanDuration()
141
    {
142
        $duration = WebRequest::postString('duration');
143
        if ($duration === "other") {
144
            $duration = strtotime(WebRequest::postString('otherduration'));
145
146
            if (!$duration) {
147
                throw new ApplicationLogicException('Invalid ban time');
148
            }
149
            elseif (time() > $duration) {
150
                throw new ApplicationLogicException('Ban time has already expired!');
151
            }
152
153
            return $duration;
154
        }
155
        elseif ($duration === "-1") {
156
            return null;
157
        }
158
        else {
159
            $duration = WebRequest::postInt('duration') + time();
160
161
            return $duration;
162
        }
163
    }
164
165
    /**
166
     * Handles the POST method on the set action
167
     *
168
     * @throws ApplicationLogicException
169
     * @throws Exception
170
     */
171
    private function handlePostMethodForSetBan()
172
    {
173
        $this->validateCSRFToken();
174
        $database = $this->getDatabase();
175
        $user = User::getCurrent($database);
176
177
        // Checks whether there is a reason entered for ban.
178
        $reason = WebRequest::postString('banreason');
179
        if ($reason === null || trim($reason) === "") {
180
            throw new ApplicationLogicException('You must specify a ban reason');
181
        }
182
183
        // ban targets
184
        $targetName = WebRequest::postString('banName');
185
        $targetIp = WebRequest::postString('banIP');
186
        $targetEmail = WebRequest::postString('banEmail');
187
        $targetUseragent = WebRequest::postString('banUseragent');
188
        $targetMask = null;
189
190
        // check the user is allowed to use provided targets
191
        if(!$this->barrierTest('name', $user, 'BanType')) {
192
            $targetName = null;
193
        }
194
        if(!$this->barrierTest('ip', $user, 'BanType')) {
195
            $targetIp = null;
196
        }
197
        if(!$this->barrierTest('email', $user, 'BanType')) {
198
            $targetEmail = null;
199
        }
200
        if(!$this->barrierTest('useragent', $user, 'BanType')) {
201
            $targetUseragent = null;
202
        }
203
204
        // Checks whether there is a target entered to ban.
205
        if ($targetName === null && $targetIp === null && $targetEmail === null && $targetUseragent === null) {
206
            throw new ApplicationLogicException('You must specify a target to be banned');
207
        }
208
209
        // Validate ban duration
210
        $duration = $this->getBanDuration();
211
212
        // handle CIDR ranges
213
        if ($targetIp !== null) {
214
            if (strpos($targetIp, '/') !== false) {
215
                $ipParts = explode('/', $targetIp, 2);
216
                $targetIp = $ipParts[0];
217
                $targetMask = $ipParts[1];
218
            } else {
219
                $targetMask = filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? 128 : 32;
220
            }
221
222
            $this->validateIpBan($targetIp, $targetMask);
223
        }
224
225
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
226
        if (count($banHelper->getBansByTarget($targetName, $targetEmail, $targetIp, $targetMask, $targetUseragent)) > 0) {
0 ignored issues
show
Bug introduced by
It seems like $targetMask can also be of type string; however, parameter $mask of Waca\Helpers\BanHelper::getBansByTarget() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

226
        if (count($banHelper->getBansByTarget($targetName, $targetEmail, $targetIp, /** @scrutinizer ignore-type */ $targetMask, $targetUseragent)) > 0) {
Loading history...
227
            throw new ApplicationLogicException('This target is already banned!');
228
        }
229
230
        $ban = new Ban();
231
        $ban->setDatabase($database);
232
        $ban->setActive(true);
233
234
        $ban->setName($targetName);
235
        $ban->setIp($targetIp, $targetMask);
0 ignored issues
show
Bug introduced by
It seems like $targetMask can also be of type string; however, parameter $mask of Waca\DataObjects\Ban::setIp() does only seem to accept integer|null, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

235
        $ban->setIp($targetIp, /** @scrutinizer ignore-type */ $targetMask);
Loading history...
236
        $ban->setEmail($targetEmail);
237
        $ban->setUseragent($targetUseragent);
238
239
        $ban->setUser($user->getId());
240
        $ban->setReason($reason);
241
        $ban->setDuration($duration);
242
243
        $ban->setAction(WebRequest::postString('banAction'));
0 ignored issues
show
Bug introduced by
It seems like Waca\WebRequest::postString('banAction') can also be of type null; however, parameter $action of Waca\DataObjects\Ban::setAction() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

243
        $ban->setAction(/** @scrutinizer ignore-type */ WebRequest::postString('banAction'));
Loading history...
244
        if ($ban->getAction() === Ban::ACTION_DEFER) {
245
            $ban->setActionTarget(WebRequest::postString('banActionTarget'));
246
        }
247
248
        $ban->save();
249
250
        Logger::banned($database, $ban, $reason);
251
252
        $this->getNotificationHelper()->banned($ban);
253
        SessionAlert::quick('Ban has been set.');
254
255
        $this->redirect('bans');
256
    }
257
258
    /**
259
     * Handles the GET method on the set action
260
     * @throws Exception
261
     */
262
    protected function handleGetMethodForSetBan()
263
    {
264
        $this->setTemplate('bans/banform.tpl');
265
        $this->assignCSRFToken();
266
267
        $database = $this->getDatabase();
268
269
        $user = User::getCurrent($database);
270
        $this->setupSecurity($user);
271
272
        $this->assign('requestStates', $this->getSiteConfiguration()->getRequestStates());
273
274
        $banType = WebRequest::getString('type');
275
        $banRequest = WebRequest::getInt('request');
276
277
        // if the parameters are null, skip loading a request.
278
        if ($banType === null || $banRequest === null || $banRequest === 0) {
279
            return;
280
        }
281
282
        // Attempt to resolve the correct target
283
        /** @var Request $request */
284
        $request = Request::getById($banRequest, $database);
285
        if ($request === false) {
0 ignored issues
show
introduced by
The condition $request === false is always false.
Loading history...
286
            $this->assign('bantarget', '');
287
288
            return;
289
        }
290
291
        switch ($banType) {
292
            case 'EMail':
293
                if ($this->barrierTest('email', $user, 'BanType')) {
294
                    $this->assign('banEmail', $request->getEmail());
295
                }
296
                break;
297
            case 'IP':
298
                if ($this->barrierTest('ip', $user, 'BanType')) {
299
                    $this->assign('banIP', $this->getXffTrustProvider()
300
                        ->getTrustedClientIp($request->getIp(), $request->getForwardedIp()));
301
                }
302
                break;
303
            case 'Name':
304
                if ($this->barrierTest('username', $user, 'BanType')) {
305
                    $this->assign('banName', $request->getEmail());
306
                }
307
                break;
308
            case 'UA':
309
                if ($this->barrierTest('useragent', $user, 'BanType')) {
310
                    $this->assign('banUseragent', $request->getEmail());
311
                }
312
                break;
313
        }
314
    }
315
316
    /**
317
     * @return Ban
318
     * @throws ApplicationLogicException
319
     */
320
    private function getBanForUnban()
321
    {
322
        $banId = WebRequest::getInt('id');
323
        if ($banId === null || $banId === 0) {
324
            throw new ApplicationLogicException("The ban ID appears to be missing. This is probably a bug.");
325
        }
326
327
        $database = $this->getDatabase();
328
        $this->setupSecurity(User::getCurrent($database));
329
        $ban = Ban::getActiveId($banId, $database);
330
331
        if ($ban === false) {
0 ignored issues
show
introduced by
The condition $ban === false is always false.
Loading history...
332
            throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
333
        }
334
335
        return $ban;
336
    }
337
338
    /**
339
     * @param $user
340
     */
341
    protected function setupSecurity($user): void
342
    {
343
        $this->assign('canSeeIpBan', $this->barrierTest('ip', $user, 'BanType'));
344
        $this->assign('canSeeNameBan', $this->barrierTest('name', $user, 'BanType'));
345
        $this->assign('canSeeEmailBan', $this->barrierTest('email', $user, 'BanType'));
346
        $this->assign('canSeeUseragentBan', $this->barrierTest('useragent', $user, 'BanType'));
347
    }
348
349
    /**
350
     * @param string $targetIp
351
     * @param        $targetMask
352
     *
353
     * @throws ApplicationLogicException
354
     */
355
    private function validateIpBan(string $targetIp, $targetMask): void
356
    {
357
        // validate this is an IP
358
        if (!filter_var($targetIp, FILTER_VALIDATE_IP)) {
359
            throw new ApplicationLogicException("Not a valid IP address");
360
        }
361
362
        // validate CIDR ranges
363
        if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && ($targetMask < 0 || $targetMask > 128)) {
364
            throw new ApplicationLogicException("CIDR mask out of range for IPv6");
365
        }
366
367
        if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && ($targetMask < 0 || $targetMask > 32)) {
368
            throw new ApplicationLogicException("CIDR mask out of range for IPv4");
369
        }
370
371
        $squidIpList = $this->getSiteConfiguration()->getSquidList();
372
        if (in_array($targetIp, $squidIpList)) {
373
            throw new ApplicationLogicException("This IP address is on the protected list of proxies, and cannot be banned.");
374
        }
375
    }
376
377
    /**
378
     * @param array $bans
379
     */
380
    protected function setupBanList(array $bans): void
381
    {
382
        $userIds = array_map(
383
            function(Ban $entry) {
384
                return $entry->getUser();
385
            },
386
            $bans);
387
        $userList = UserSearchHelper::get($this->getDatabase())->inIds($userIds)->fetchMap('username');
388
389
        $user = User::getCurrent($this->getDatabase());
390
        $this->assign('canSet', $this->barrierTest('set', $user));
391
        $this->assign('canRemove', $this->barrierTest('remove', $user));
392
393
        $this->setupSecurity($user);
394
395
        $this->assign('usernames', $userList);
396
        $this->assign('activebans', $bans);
397
398
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
399
        $this->assign('banHelper', $banHelper);
400
    }
401
}
402