PageBan   F
last analyzed

Complexity

Total Complexity 80

Size/Duplication

Total Lines 471
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 80
eloc 217
c 2
b 0
f 0
dl 0
loc 471
rs 2

14 Methods

Rating   Name   Duplication   Size   Complexity  
D validateIpBan() 0 47 21
A main() 0 11 1
B handlePostMethodForSetBan() 0 71 8
A splitCidrRange() 0 12 3
A getBanVisibility() 0 8 4
A setupSecurity() 0 10 1
A getBanForUnban() 0 16 4
A getBanDuration() 0 22 5
A remove() 0 42 5
C handleGetMethodForSetBan() 0 56 13
B getRawBanTargets() 0 27 9
A show() 0 19 2
A setupBanList() 0 20 1
A set() 0 16 3

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\RequestQueue;
16
use Waca\DataObjects\User;
17
use Waca\Exceptions\AccessDeniedException;
18
use Waca\Exceptions\ApplicationLogicException;
19
use Waca\Helpers\BanHelper;
20
use Waca\Helpers\Logger;
21
use Waca\Helpers\SearchHelpers\UserSearchHelper;
22
use Waca\SessionAlert;
23
use Waca\Tasks\InternalPageBase;
24
use Waca\WebRequest;
25
26
class PageBan extends InternalPageBase
27
{
28
    /**
29
     * Main function for this page, when no specific actions are called.
30
     */
31
    protected function main()
32
    {
33
        $this->assignCSRFToken();
34
        $this->setHtmlTitle('Bans');
35
36
        $bans = Ban::getActiveBans($this->getDatabase());
37
38
        $this->setupBanList($bans);
39
40
        $this->assign('isFiltered', false);
41
        $this->setTemplate('bans/main.tpl');
42
    }
43
44
    protected function show()
45
    {
46
        $this->assignCSRFToken();
47
        $this->setHtmlTitle('Bans');
48
49
        $rawIdList = WebRequest::getString('id');
50
        if ($rawIdList === null) {
51
            $this->redirect('bans');
52
53
            return;
54
        }
55
56
        $idList = explode(',', $rawIdList);
57
58
        $bans = Ban::getByIdList($idList, $this->getDatabase());
59
60
        $this->setupBanList($bans);
61
        $this->assign('isFiltered', true);
62
        $this->setTemplate('bans/main.tpl');
63
    }
64
65
    /**
66
     * Entry point for the ban set action
67
     * @throws SmartyException
68
     * @throws Exception
69
     */
70
    protected function set()
71
    {
72
        $this->setHtmlTitle('Bans');
73
74
        // dual-mode action
75
        if (WebRequest::wasPosted()) {
76
            try {
77
                $this->handlePostMethodForSetBan();
78
            }
79
            catch (ApplicationLogicException $ex) {
80
                SessionAlert::error($ex->getMessage());
81
                $this->redirect("bans", "set");
82
            }
83
        }
84
        else {
85
            $this->handleGetMethodForSetBan();
86
        }
87
    }
88
89
    /**
90
     * Entry point for the ban remove action
91
     *
92
     * @throws AccessDeniedException
93
     * @throws ApplicationLogicException
94
     * @throws SmartyException
95
     */
96
    protected function remove()
97
    {
98
        $this->setHtmlTitle('Bans');
99
100
        $ban = $this->getBanForUnban();
101
102
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
103
        if (!$banHelper->canUnban($ban)) {
104
            // triggered when a user tries to unban a ban they can't see the entirety of.
105
            // there's no UI way to get to this, so a raw exception is fine.
106
            throw new AccessDeniedException($this->getSecurityManager(), $this->getDomainAccessManager());
107
        }
108
109
        // dual mode
110
        if (WebRequest::wasPosted()) {
111
            $this->validateCSRFToken();
112
            $unbanReason = WebRequest::postString('unbanreason');
113
114
            if ($unbanReason === null || trim($unbanReason) === "") {
115
                SessionAlert::error('No unban reason specified');
116
                $this->redirect("bans", "remove", array('id' => $ban->getId()));
117
            }
118
119
            // set optimistic locking from delete form page load
120
            $updateVersion = WebRequest::postInt('updateversion');
121
            $ban->setUpdateVersion($updateVersion);
122
123
            $database = $this->getDatabase();
124
            $ban->setActive(false);
125
            $ban->save();
126
127
            Logger::unbanned($database, $ban, $unbanReason);
128
129
            SessionAlert::quick('Disabled ban.');
130
            $this->getNotificationHelper()->unbanned($ban, $unbanReason);
131
132
            $this->redirect('bans');
133
        }
134
        else {
135
            $this->assignCSRFToken();
136
            $this->assign('ban', $ban);
137
            $this->setTemplate('bans/unban.tpl');
138
        }
139
    }
140
141
    /**
142
     * @throws ApplicationLogicException
143
     */
144
    private function getBanDuration()
145
    {
146
        $duration = WebRequest::postString('duration');
147
        if ($duration === "other") {
148
            $duration = strtotime(WebRequest::postString('otherduration'));
0 ignored issues
show
Bug introduced by
It seems like Waca\WebRequest::postString('otherduration') can also be of type null; however, parameter $datetime of strtotime() 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

148
            $duration = strtotime(/** @scrutinizer ignore-type */ WebRequest::postString('otherduration'));
Loading history...
149
150
            if (!$duration) {
151
                throw new ApplicationLogicException('Invalid ban time');
152
            }
153
            elseif (time() > $duration) {
154
                throw new ApplicationLogicException('Ban time has already expired!');
155
            }
156
157
            return $duration;
158
        }
159
        elseif ($duration === "-1") {
160
            return null;
161
        }
162
        else {
163
            $duration = WebRequest::postInt('duration') + time();
164
165
            return $duration;
166
        }
167
    }
168
169
    /**
170
     * Handles the POST method on the set action
171
     *
172
     * @throws ApplicationLogicException
173
     * @throws Exception
174
     */
175
    private function handlePostMethodForSetBan()
176
    {
177
        $this->validateCSRFToken();
178
        $database = $this->getDatabase();
179
        $user = User::getCurrent($database);
180
181
        // Checks whether there is a reason entered for ban.
182
        $reason = WebRequest::postString('banreason');
183
        if ($reason === null || trim($reason) === "") {
184
            throw new ApplicationLogicException('You must specify a ban reason');
185
        }
186
187
        // ban targets
188
        list($targetName, $targetIp, $targetEmail, $targetUseragent) = $this->getRawBanTargets($user);
189
190
        $visibility = $this->getBanVisibility();
191
192
        // Validate ban duration
193
        $duration = $this->getBanDuration();
194
195
        $action = WebRequest::postString('banAction') ?? Ban::ACTION_NONE;
196
197
        // handle CIDR ranges
198
        $targetMask = null;
199
        if ($targetIp !== null) {
200
            list($targetIp, $targetMask) = $this->splitCidrRange($targetIp);
201
            $this->validateIpBan($targetIp, $targetMask, $user, $action);
202
        }
203
204
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
205
        if (count($banHelper->getBansByTarget($targetName, $targetEmail, $targetIp, $targetMask, $targetUseragent)) > 0) {
206
            throw new ApplicationLogicException('This target is already banned!');
207
        }
208
209
        $ban = new Ban();
210
        $ban->setDatabase($database);
211
        $ban->setActive(true);
212
213
        $ban->setName($targetName);
214
        $ban->setIp($targetIp, $targetMask);
215
        $ban->setEmail($targetEmail);
216
        $ban->setUseragent($targetUseragent);
217
218
        $ban->setUser($user->getId());
219
        $ban->setReason($reason);
220
        $ban->setDuration($duration);
221
        $ban->setVisibility($visibility);
222
223
        $ban->setAction($action);
224
        if ($ban->getAction() === Ban::ACTION_DEFER) {
225
            //FIXME: domains
226
            $queue = RequestQueue::getByApiName($database, WebRequest::postString('banActionTarget'), 1);
0 ignored issues
show
Bug introduced by
It seems like Waca\WebRequest::postString('banActionTarget') can also be of type null; however, parameter $apiName of Waca\DataObjects\RequestQueue::getByApiName() 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

226
            $queue = RequestQueue::getByApiName($database, /** @scrutinizer ignore-type */ WebRequest::postString('banActionTarget'), 1);
Loading history...
227
            if ($queue === false) {
228
                throw new ApplicationLogicException("Unknown target queue");
229
            }
230
231
            if (!$queue->isEnabled()) {
232
                throw new ApplicationLogicException("Target queue is not enabled");
233
            }
234
235
            $ban->setTargetQueue($queue->getId());
236
        }
237
238
        $ban->save();
239
240
        Logger::banned($database, $ban, $reason);
241
242
        $this->getNotificationHelper()->banned($ban);
243
        SessionAlert::quick('Ban has been set.');
244
245
        $this->redirect('bans');
246
    }
247
248
    /**
249
     * Handles the GET method on the set action
250
     * @throws Exception
251
     */
252
    protected function handleGetMethodForSetBan()
253
    {
254
        $this->setTemplate('bans/banform.tpl');
255
        $this->assignCSRFToken();
256
257
        $this->assign('maxIpRange', $this->getSiteConfiguration()->getBanMaxIpRange());
258
        $this->assign('maxIpBlockRange', $this->getSiteConfiguration()->getBanMaxIpBlockRange());
259
260
        $database = $this->getDatabase();
261
262
        $user = User::getCurrent($database);
263
        $this->setupSecurity($user);
264
265
        $queues = RequestQueue::getEnabledQueues($database);
266
267
        $this->assign('requestQueues', $queues);
268
269
        $banType = WebRequest::getString('type');
270
        $banRequest = WebRequest::getInt('request');
271
272
        // if the parameters are null, skip loading a request.
273
        if ($banType === null || $banRequest === null || $banRequest === 0) {
274
            return;
275
        }
276
277
        // Attempt to resolve the correct target
278
        /** @var Request|false $request */
279
        $request = Request::getById($banRequest, $database);
280
        if ($request === false) {
0 ignored issues
show
introduced by
The condition $request === false is always false.
Loading history...
281
            $this->assign('bantarget', '');
282
283
            return;
284
        }
285
286
        switch ($banType) {
287
            case 'EMail':
288
                if ($this->barrierTest('email', $user, 'BanType')) {
289
                    $this->assign('banEmail', $request->getEmail());
290
                }
291
                break;
292
            case 'IP':
293
                if ($this->barrierTest('ip', $user, 'BanType')) {
294
                    $this->assign('banIP', $this->getXffTrustProvider()
295
                        ->getTrustedClientIp($request->getIp(), $request->getForwardedIp()));
296
                }
297
                break;
298
            case 'Name':
299
                if ($this->barrierTest('name', $user, 'BanType')) {
300
                    $this->assign('banName', $request->getName());
301
                }
302
                break;
303
            case 'UA':
304
                if ($this->barrierTest('useragent', $user, 'BanType')) {
305
                    $this->assign('banUseragent', $request->getEmail());
306
                }
307
                break;
308
        }
309
    }
310
311
    /**
312
     * @return Ban
313
     * @throws ApplicationLogicException
314
     */
315
    private function getBanForUnban()
316
    {
317
        $banId = WebRequest::getInt('id');
318
        if ($banId === null || $banId === 0) {
319
            throw new ApplicationLogicException("The ban ID appears to be missing. This is probably a bug.");
320
        }
321
322
        $database = $this->getDatabase();
323
        $this->setupSecurity(User::getCurrent($database));
324
        $ban = Ban::getActiveId($banId, $database);
325
326
        if ($ban === false) {
327
            throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
328
        }
329
330
        return $ban;
331
    }
332
333
    /**
334
     * @param $user
335
     */
336
    protected function setupSecurity($user): void
337
    {
338
        $this->assign('canSeeIpBan', $this->barrierTest('ip', $user, 'BanType'));
339
        $this->assign('canSeeNameBan', $this->barrierTest('name', $user, 'BanType'));
340
        $this->assign('canSeeEmailBan', $this->barrierTest('email', $user, 'BanType'));
341
        $this->assign('canSeeUseragentBan', $this->barrierTest('useragent', $user, 'BanType'));
342
343
        $this->assign('canSeeUserVisibility', $this->barrierTest('user', $user, 'BanVisibility'));
344
        $this->assign('canSeeAdminVisibility', $this->barrierTest('admin', $user, 'BanVisibility'));
345
        $this->assign('canSeeCheckuserVisibility', $this->barrierTest('checkuser', $user, 'BanVisibility'));
346
    }
347
348
    /**
349
     * @param string $targetIp
350
     * @param        $targetMask
351
     * @param User   $user
352
     * @param        $action
353
     *
354
     * @throws ApplicationLogicException
355
     */
356
    private function validateIpBan(string $targetIp, $targetMask, User $user, $action): void
357
    {
358
        // validate this is an IP
359
        if (!filter_var($targetIp, FILTER_VALIDATE_IP)) {
360
            throw new ApplicationLogicException("Not a valid IP address");
361
        }
362
363
        $canLargeIpBan = $this->barrierTest('ip-largerange', $user, 'BanType');
364
        $maxIpBlockRange = $this->getSiteConfiguration()->getBanMaxIpBlockRange();
365
        $maxIpRange = $this->getSiteConfiguration()->getBanMaxIpRange();
366
367
        // validate CIDR ranges
368
        if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
369
            if ($targetMask < 0 || $targetMask > 128) {
370
                throw new ApplicationLogicException("CIDR mask out of range for IPv6");
371
            }
372
373
            // prevent setting the ban if:
374
            //  * the user isn't allowed to set large bans, AND
375
            //  * the ban is a drop or a block (preventing human review of the request), AND
376
            //  * the mask is too wide-reaching
377
            if (!$canLargeIpBan && ($action == Ban::ACTION_BLOCK || $action == Ban::ACTION_DROP) && $targetMask < $maxIpBlockRange[6]) {
378
                throw new ApplicationLogicException("The requested IP range for this ban is too wide for the block/drop action.");
379
            }
380
381
            if (!$canLargeIpBan && $targetMask < $maxIpRange[6]) {
382
                throw new ApplicationLogicException("The requested IP range for this ban is too wide.");
383
            }
384
        }
385
386
        if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
387
            if ($targetMask < 0 || $targetMask > 32) {
388
                throw new ApplicationLogicException("CIDR mask out of range for IPv4");
389
            }
390
391
            if (!$canLargeIpBan && ($action == Ban::ACTION_BLOCK || $action == Ban::ACTION_DROP) && $targetMask < $maxIpBlockRange[4]) {
392
                throw new ApplicationLogicException("The IP range for this ban is too wide for the block/drop action.");
393
            }
394
395
            if (!$canLargeIpBan && $targetMask < $maxIpRange[4]) {
396
                throw new ApplicationLogicException("The requested IP range for this ban is too wide.");
397
            }
398
        }
399
400
        $squidIpList = $this->getSiteConfiguration()->getSquidList();
401
        if (in_array($targetIp, $squidIpList)) {
402
            throw new ApplicationLogicException("This IP address is on the protected list of proxies, and cannot be banned.");
403
        }
404
    }
405
406
    /**
407
     * @param array $bans
408
     */
409
    protected function setupBanList(array $bans): void
410
    {
411
        $userIds = array_map(
412
            function(Ban $entry) {
413
                return $entry->getUser();
414
            },
415
            $bans);
416
        $userList = UserSearchHelper::get($this->getDatabase())->inIds($userIds)->fetchMap('username');
417
418
        $user = User::getCurrent($this->getDatabase());
419
        $this->assign('canSet', $this->barrierTest('set', $user));
420
        $this->assign('canRemove', $this->barrierTest('remove', $user));
421
422
        $this->setupSecurity($user);
423
424
        $this->assign('usernames', $userList);
425
        $this->assign('activebans', $bans);
426
427
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
428
        $this->assign('banHelper', $banHelper);
429
    }
430
431
    /**
432
     * @param string $targetIp
433
     *
434
     * @return array
435
     */
436
    private function splitCidrRange(string $targetIp): array
437
    {
438
        if (strpos($targetIp, '/') !== false) {
439
            $ipParts = explode('/', $targetIp, 2);
440
            $targetIp = $ipParts[0];
441
            $targetMask = (int)$ipParts[1];
442
        }
443
        else {
444
            $targetMask = filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? 128 : 32;
445
        }
446
447
        return array($targetIp, $targetMask);
448
}
449
450
    /**
451
     * @return string|null
452
     * @throws ApplicationLogicException
453
     */
454
    private function getBanVisibility()
455
    {
456
        $visibility = WebRequest::postString('banVisibility');
457
        if ($visibility !== 'user' && $visibility !== 'admin' && $visibility !== 'checkuser') {
458
            throw new ApplicationLogicException('Invalid ban visibility');
459
        }
460
461
        return $visibility;
462
    }
463
464
    /**
465
     * @param $user
466
     *
467
     * @return array
468
     * @throws ApplicationLogicException
469
     */
470
    private function getRawBanTargets($user): array
471
    {
472
        $targetName = WebRequest::postString('banName');
473
        $targetIp = WebRequest::postString('banIP');
474
        $targetEmail = WebRequest::postString('banEmail');
475
        $targetUseragent = WebRequest::postString('banUseragent');
476
477
        // check the user is allowed to use provided targets
478
        if (!$this->barrierTest('name', $user, 'BanType')) {
479
            $targetName = null;
480
        }
481
        if (!$this->barrierTest('ip', $user, 'BanType')) {
482
            $targetIp = null;
483
        }
484
        if (!$this->barrierTest('email', $user, 'BanType')) {
485
            $targetEmail = null;
486
        }
487
        if (!$this->barrierTest('useragent', $user, 'BanType')) {
488
            $targetUseragent = null;
489
        }
490
491
        // Checks whether there is a target entered to ban.
492
        if ($targetName === null && $targetIp === null && $targetEmail === null && $targetUseragent === null) {
493
            throw new ApplicationLogicException('You must specify a target to be banned');
494
        }
495
496
        return array($targetName, $targetIp, $targetEmail, $targetUseragent);
497
}
498
}
499