Passed
Push — bantools ( 1cbaa6...d648f7 )
by Simon
04:04
created

PageBan::validateIpBan()   D

Complexity

Conditions 21
Paths 18

Size

Total Lines 47
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 462

Importance

Changes 0
Metric Value
eloc 22
c 0
b 0
f 0
dl 0
loc 47
ccs 0
cts 33
cp 0
rs 4.1666
cc 21
nc 18
nop 4
crap 462

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
39
        $this->assign('isFiltered', false);
40
        $this->setTemplate('bans/main.tpl');
41
    }
42
43
    protected function show()
44
    {
45
        $this->assignCSRFToken();
46
        $this->setHtmlTitle('Bans');
47
48
        $rawIdList = WebRequest::getString('id');
49
        if ($rawIdList === null) {
50
            $this->redirect('bans');
51
52
            return;
53
        }
54
55
        $idList = explode(',', $rawIdList);
56
57
        $bans = Ban::getByIdList($idList, $this->getDatabase());
58
59
        $this->setupBanList($bans);
60
        $this->assign('isFiltered', true);
61
        $this->setTemplate('bans/main.tpl');
62
    }
63
64
    /**
65
     * Entry point for the ban set action
66
     * @throws SmartyException
67
     * @throws Exception
68
     */
69
    protected function set()
70
    {
71
        $this->setHtmlTitle('Bans');
72
73
        // dual-mode action
74
        if (WebRequest::wasPosted()) {
75
            try {
76
                $this->handlePostMethodForSetBan();
77
            }
78
            catch (ApplicationLogicException $ex) {
79
                SessionAlert::error($ex->getMessage());
80
                $this->redirect("bans", "set");
81
            }
82
        }
83
        else {
84
            $this->handleGetMethodForSetBan();
85
        }
86
    }
87
88
    /**
89
     * Entry point for the ban remove action
90
     *
91
     * @throws AccessDeniedException
92
     * @throws ApplicationLogicException
93
     * @throws SmartyException
94
     */
95
    protected function remove()
96
    {
97
        $this->setHtmlTitle('Bans');
98
99
        $ban = $this->getBanForUnban();
100
101
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
102
        if (!$banHelper->canUnban($ban)) {
103
            // triggered when a user tries to unban a ban they can't see the entirety of.
104
            // there's no UI way to get to this, so a raw exception is fine.
105
            throw new AccessDeniedException($this->getSecurityManager());
106
        }
107
108
        // dual mode
109
        if (WebRequest::wasPosted()) {
110
            $this->validateCSRFToken();
111
            $unbanReason = WebRequest::postString('unbanreason');
112
113
            if ($unbanReason === null || trim($unbanReason) === "") {
114
                SessionAlert::error('No unban reason specified');
115
                $this->redirect("bans", "remove", array('id' => $ban->getId()));
116
            }
117
118
            // set optimistic locking from delete form page load
119
            $updateVersion = WebRequest::postInt('updateversion');
120
            $ban->setUpdateVersion($updateVersion);
121
122
            $database = $this->getDatabase();
123
            $ban->setActive(false);
124
            $ban->save();
125
126
            Logger::unbanned($database, $ban, $unbanReason);
127
128
            SessionAlert::quick('Disabled ban.');
129
            $this->getNotificationHelper()->unbanned($ban, $unbanReason);
130
131
            $this->redirect('bans');
132
        }
133
        else {
134
            $this->assignCSRFToken();
135
            $this->assign('ban', $ban);
136
            $this->setTemplate('bans/unban.tpl');
137
        }
138
    }
139
140
    /**
141
     * @throws ApplicationLogicException
142
     */
143
    private function getBanDuration()
144
    {
145
        $duration = WebRequest::postString('duration');
146
        if ($duration === "other") {
147
            $duration = strtotime(WebRequest::postString('otherduration'));
148
149
            if (!$duration) {
150
                throw new ApplicationLogicException('Invalid ban time');
151
            }
152
            elseif (time() > $duration) {
153
                throw new ApplicationLogicException('Ban time has already expired!');
154
            }
155
156
            return $duration;
157
        }
158
        elseif ($duration === "-1") {
159
            return null;
160
        }
161
        else {
162
            $duration = WebRequest::postInt('duration') + time();
163
164
            return $duration;
165
        }
166
    }
167
168
    /**
169
     * Handles the POST method on the set action
170
     *
171
     * @throws ApplicationLogicException
172
     * @throws Exception
173
     */
174
    private function handlePostMethodForSetBan()
175
    {
176
        $this->validateCSRFToken();
177
        $database = $this->getDatabase();
178
        $user = User::getCurrent($database);
179
180
        // Checks whether there is a reason entered for ban.
181
        $reason = WebRequest::postString('banreason');
182
        if ($reason === null || trim($reason) === "") {
183
            throw new ApplicationLogicException('You must specify a ban reason');
184
        }
185
186
        // ban targets
187
        list($targetName, $targetIp, $targetEmail, $targetUseragent) = $this->getRawBanTargets($user);
188
189
        $visibility = $this->getBanVisibility();
190
191
        // Validate ban duration
192
        $duration = $this->getBanDuration();
193
194
        $action = WebRequest::postString('banAction') ?? Ban::ACTION_NONE;
195
196
        // handle CIDR ranges
197
        $targetMask = null;
198
        if ($targetIp !== null) {
199
            list($targetIp, $targetMask) = $this->splitCidrRange($targetIp);
200
            $this->validateIpBan($targetIp, $targetMask, $user, $action);
201
        }
202
203
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
204
        if (count($banHelper->getBansByTarget($targetName, $targetEmail, $targetIp, $targetMask, $targetUseragent)) > 0) {
205
            throw new ApplicationLogicException('This target is already banned!');
206
        }
207
208
        $ban = new Ban();
209
        $ban->setDatabase($database);
210
        $ban->setActive(true);
211
212
        $ban->setName($targetName);
213
        $ban->setIp($targetIp, $targetMask);
214
        $ban->setEmail($targetEmail);
215
        $ban->setUseragent($targetUseragent);
216
217
        $ban->setUser($user->getId());
218
        $ban->setReason($reason);
219
        $ban->setDuration($duration);
220
        $ban->setVisibility($visibility);
221
222
        $ban->setAction($action);
223
        if ($ban->getAction() === Ban::ACTION_DEFER) {
224
            $ban->setActionTarget(WebRequest::postString('banActionTarget'));
225
        }
226
227
        $ban->save();
228
229
        Logger::banned($database, $ban, $reason);
230
231
        $this->getNotificationHelper()->banned($ban);
232
        SessionAlert::quick('Ban has been set.');
233
234
        $this->redirect('bans');
235
    }
236
237
    /**
238
     * Handles the GET method on the set action
239
     * @throws Exception
240
     */
241
    protected function handleGetMethodForSetBan()
242
    {
243
        $this->setTemplate('bans/banform.tpl');
244
        $this->assignCSRFToken();
245
246
        $this->assign('maxIpRange', $this->getSiteConfiguration()->getBanMaxIpRange());
247
        $this->assign('maxIpBlockRange', $this->getSiteConfiguration()->getBanMaxIpBlockRange());
248
249
        $database = $this->getDatabase();
250
251
        $user = User::getCurrent($database);
252
        $this->setupSecurity($user);
253
254
        $this->assign('requestStates', $this->getSiteConfiguration()->getRequestStates());
255
256
        $banType = WebRequest::getString('type');
257
        $banRequest = WebRequest::getInt('request');
258
259
        // if the parameters are null, skip loading a request.
260
        if ($banType === null || $banRequest === null || $banRequest === 0) {
261
            return;
262
        }
263
264
        // Attempt to resolve the correct target
265
        /** @var Request|false $request */
266
        $request = Request::getById($banRequest, $database);
267
        if ($request === false) {
0 ignored issues
show
introduced by
The condition $request === false is always false.
Loading history...
268
            $this->assign('bantarget', '');
269
270
            return;
271
        }
272
273
        switch ($banType) {
274
            case 'EMail':
275
                if ($this->barrierTest('email', $user, 'BanType')) {
276
                    $this->assign('banEmail', $request->getEmail());
277
                }
278
                break;
279
            case 'IP':
280
                if ($this->barrierTest('ip', $user, 'BanType')) {
281
                    $this->assign('banIP', $this->getXffTrustProvider()
282
                        ->getTrustedClientIp($request->getIp(), $request->getForwardedIp()));
283
                }
284
                break;
285
            case 'Name':
286
                if ($this->barrierTest('name', $user, 'BanType')) {
287
                    $this->assign('banName', $request->getName());
288
                }
289
                break;
290
            case 'UA':
291
                if ($this->barrierTest('useragent', $user, 'BanType')) {
292
                    $this->assign('banUseragent', $request->getEmail());
293
                }
294
                break;
295
        }
296
    }
297
298
    /**
299
     * @return Ban
300
     * @throws ApplicationLogicException
301
     */
302
    private function getBanForUnban()
303
    {
304
        $banId = WebRequest::getInt('id');
305
        if ($banId === null || $banId === 0) {
306
            throw new ApplicationLogicException("The ban ID appears to be missing. This is probably a bug.");
307
        }
308
309
        $database = $this->getDatabase();
310
        $this->setupSecurity(User::getCurrent($database));
311
        $ban = Ban::getActiveId($banId, $database);
312
313
        if ($ban === false) {
314
            throw new ApplicationLogicException("The specified ban is not currently active, or doesn't exist.");
315
        }
316
317
        return $ban;
318
    }
319
320
    /**
321
     * @param $user
322
     */
323
    protected function setupSecurity($user): void
324
    {
325
        $this->assign('canSeeIpBan', $this->barrierTest('ip', $user, 'BanType'));
326
        $this->assign('canSeeNameBan', $this->barrierTest('name', $user, 'BanType'));
327
        $this->assign('canSeeEmailBan', $this->barrierTest('email', $user, 'BanType'));
328
        $this->assign('canSeeUseragentBan', $this->barrierTest('useragent', $user, 'BanType'));
329
330
        $this->assign('canSeeUserVisibility', $this->barrierTest('user', $user, 'BanVisibility'));
331
        $this->assign('canSeeAdminVisibility', $this->barrierTest('admin', $user, 'BanVisibility'));
332
        $this->assign('canSeeCheckuserVisibility', $this->barrierTest('checkuser', $user, 'BanVisibility'));
333
    }
334
335
    /**
336
     * @param string $targetIp
337
     * @param        $targetMask
338
     * @param User   $user
339
     * @param        $action
340
     *
341
     * @throws ApplicationLogicException
342
     */
343
    private function validateIpBan(string $targetIp, $targetMask, User $user, $action): void
344
    {
345
        // validate this is an IP
346
        if (!filter_var($targetIp, FILTER_VALIDATE_IP)) {
347
            throw new ApplicationLogicException("Not a valid IP address");
348
        }
349
350
        $canLargeIpBan = $this->barrierTest('ip-largerange', $user, 'BanType');
351
        $maxIpBlockRange = $this->getSiteConfiguration()->getBanMaxIpBlockRange();
352
        $maxIpRange = $this->getSiteConfiguration()->getBanMaxIpRange();
353
354
        // validate CIDR ranges
355
        if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
356
            if ($targetMask < 0 || $targetMask > 128) {
357
                throw new ApplicationLogicException("CIDR mask out of range for IPv6");
358
            }
359
360
            // prevent setting the ban if:
361
            //  * the user isn't allowed to set large bans, AND
362
            //  * the ban is a drop or a block (preventing human review of the request), AND
363
            //  * the mask is too wide-reaching
364
            if (!$canLargeIpBan && ($action == Ban::ACTION_BLOCK || $action == Ban::ACTION_DROP) && $targetMask < $maxIpBlockRange[6]) {
365
                throw new ApplicationLogicException("The requested IP range for this ban is too wide for the block/drop action.");
366
            }
367
368
            if (!$canLargeIpBan && $targetMask < $maxIpRange[6]) {
369
                throw new ApplicationLogicException("The requested IP range for this ban is too wide.");
370
            }
371
        }
372
373
        if (filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
374
            if ($targetMask < 0 || $targetMask > 32) {
375
                throw new ApplicationLogicException("CIDR mask out of range for IPv4");
376
            }
377
378
            if (!$canLargeIpBan && ($action == Ban::ACTION_BLOCK || $action == Ban::ACTION_DROP) && $targetMask < $maxIpBlockRange[4]) {
379
                throw new ApplicationLogicException("The IP range for this ban is too wide for the block/drop action.");
380
            }
381
382
            if (!$canLargeIpBan && $targetMask < $maxIpRange[4]) {
383
                throw new ApplicationLogicException("The requested IP range for this ban is too wide.");
384
            }
385
        }
386
387
        $squidIpList = $this->getSiteConfiguration()->getSquidList();
388
        if (in_array($targetIp, $squidIpList)) {
389
            throw new ApplicationLogicException("This IP address is on the protected list of proxies, and cannot be banned.");
390
        }
391
    }
392
393
    /**
394
     * @param array $bans
395
     */
396
    protected function setupBanList(array $bans): void
397
    {
398
        $userIds = array_map(
399
            function(Ban $entry) {
400
                return $entry->getUser();
401
            },
402
            $bans);
403
        $userList = UserSearchHelper::get($this->getDatabase())->inIds($userIds)->fetchMap('username');
404
405
        $user = User::getCurrent($this->getDatabase());
406
        $this->assign('canSet', $this->barrierTest('set', $user));
407
        $this->assign('canRemove', $this->barrierTest('remove', $user));
408
409
        $this->setupSecurity($user);
410
411
        $this->assign('usernames', $userList);
412
        $this->assign('activebans', $bans);
413
414
        $banHelper = new BanHelper($this->getDatabase(), $this->getXffTrustProvider(), $this->getSecurityManager());
415
        $this->assign('banHelper', $banHelper);
416
    }
417
418
    /**
419
     * @param string $targetIp
420
     *
421
     * @return array
422
     */
423
    private function splitCidrRange(string $targetIp): array
424
    {
425
        if (strpos($targetIp, '/') !== false) {
426
            $ipParts = explode('/', $targetIp, 2);
427
            $targetIp = $ipParts[0];
428
            $targetMask = (int)$ipParts[1];
429
        }
430
        else {
431
            $targetMask = filter_var($targetIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) ? 128 : 32;
432
        }
433
434
        return array($targetIp, $targetMask);
435
}
436
437
    /**
438
     * @return string|null
439
     * @throws ApplicationLogicException
440
     */
441
    private function getBanVisibility()
442
    {
443
        $visibility = WebRequest::postString('banVisibility');
444
        if ($visibility !== 'user' && $visibility !== 'admin' && $visibility !== 'checkuser') {
445
            throw new ApplicationLogicException('Invalid ban visibility');
446
        }
447
448
        return $visibility;
449
    }
450
451
    /**
452
     * @param $user
453
     *
454
     * @return array
455
     * @throws ApplicationLogicException
456
     */
457
    private function getRawBanTargets($user): array
458
    {
459
        $targetName = WebRequest::postString('banName');
460
        $targetIp = WebRequest::postString('banIP');
461
        $targetEmail = WebRequest::postString('banEmail');
462
        $targetUseragent = WebRequest::postString('banUseragent');
463
464
        // check the user is allowed to use provided targets
465
        if (!$this->barrierTest('name', $user, 'BanType')) {
466
            $targetName = null;
467
        }
468
        if (!$this->barrierTest('ip', $user, 'BanType')) {
469
            $targetIp = null;
470
        }
471
        if (!$this->barrierTest('email', $user, 'BanType')) {
472
            $targetEmail = null;
473
        }
474
        if (!$this->barrierTest('useragent', $user, 'BanType')) {
475
            $targetUseragent = null;
476
        }
477
478
        // Checks whether there is a target entered to ban.
479
        if ($targetName === null && $targetIp === null && $targetEmail === null && $targetUseragent === null) {
480
            throw new ApplicationLogicException('You must specify a target to be banned');
481
        }
482
483
        return array($targetName, $targetIp, $targetEmail, $targetUseragent);
484
}
485
}
486