Completed
Push — multiproject/db ( 3bc833...5daa57 )
by Simon
17:59 queued 13:50
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());
0 ignored issues
show
Deprecated Code introduced by
The function Waca\SiteConfiguration::getRequestStates() has been deprecated: To be removed after dynamic queues hit production. This will need to be major point release. ( Ignorable by Annotation )

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

254
        $this->assign('requestStates', /** @scrutinizer ignore-deprecated */ $this->getSiteConfiguration()->getRequestStates());

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
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