RequestValidationHelper   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 423
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 47
eloc 178
c 2
b 0
f 0
dl 0
loc 423
rs 8.64

12 Methods

Rating   Name   Duplication   Size   Complexity  
A checkAntiSpoof() 0 30 6
A formOverride() 0 11 3
A validateOther() 0 20 3
C validateName() 0 47 9
A __construct() 0 23 1
A postSaveValidations() 0 46 5
A deferRequest() 0 17 1
A userSulExists() 0 30 3
A checkTitleBlacklist() 0 34 4
A userExists() 0 28 4
A nameRequestExists() 0 11 2
A validateEmail() 0 29 6

How to fix   Complexity   

Complex Class

Complex classes like RequestValidationHelper 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 RequestValidationHelper, 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\Validation;
10
11
use Exception;
12
use Waca\DataObjects\Ban;
13
use Waca\DataObjects\Comment;
14
use Waca\DataObjects\Domain;
15
use Waca\DataObjects\Request;
16
use Waca\DataObjects\RequestQueue;
17
use Waca\ExceptionHandler;
18
use Waca\Exceptions\CurlException;
19
use Waca\Helpers\HttpHelper;
20
use Waca\Helpers\Interfaces\IBanHelper;
21
use Waca\Helpers\Logger;
22
use Waca\PdoDatabase;
23
use Waca\Providers\Interfaces\IAntiSpoofProvider;
24
use Waca\Providers\Interfaces\IXffTrustProvider;
25
use Waca\Providers\TorExitProvider;
26
use Waca\RequestStatus;
27
use Waca\SiteConfiguration;
28
29
/**
30
 * Performs the validation of an incoming request.
31
 */
32
class RequestValidationHelper
33
{
34
    /** @var IBanHelper */
35
    private $banHelper;
36
    /** @var PdoDatabase */
37
    private $database;
38
    /** @var IAntiSpoofProvider */
39
    private $antiSpoofProvider;
40
    /** @var IXffTrustProvider */
41
    private $xffTrustProvider;
42
    /** @var HttpHelper */
43
    private $httpHelper;
44
    /**
45
     * @var string
46
     */
47
    private $mediawikiApiEndpoint;
48
    private $titleBlacklistEnabled;
49
    /**
50
     * @var TorExitProvider
51
     */
52
    private $torExitProvider;
53
    /**
54
     * @var SiteConfiguration
55
     */
56
    private $siteConfiguration;
57
58
    private $validationRemoteTimeout = 5000;
59
60
    /**
61
     * Summary of __construct
62
     *
63
     * @param IBanHelper         $banHelper
64
     * @param PdoDatabase        $database
65
     * @param IAntiSpoofProvider $antiSpoofProvider
66
     * @param IXffTrustProvider  $xffTrustProvider
67
     * @param HttpHelper         $httpHelper
68
     * @param TorExitProvider    $torExitProvider
69
     * @param SiteConfiguration  $siteConfiguration
70
     */
71
    public function __construct(
72
        IBanHelper $banHelper,
73
        PdoDatabase $database,
74
        IAntiSpoofProvider $antiSpoofProvider,
75
        IXffTrustProvider $xffTrustProvider,
76
        HttpHelper $httpHelper,
77
        TorExitProvider $torExitProvider,
78
        SiteConfiguration $siteConfiguration
79
    ) {
80
        $this->banHelper = $banHelper;
81
        $this->database = $database;
82
        $this->antiSpoofProvider = $antiSpoofProvider;
83
        $this->xffTrustProvider = $xffTrustProvider;
84
        $this->httpHelper = $httpHelper;
85
86
        // FIXME: domains!
87
        /** @var Domain $domain */
88
        $domain = Domain::getById(1, $database);
89
90
        $this->mediawikiApiEndpoint = $domain->getWikiApiPath();
91
        $this->titleBlacklistEnabled = $siteConfiguration->getTitleBlacklistEnabled();
92
        $this->torExitProvider = $torExitProvider;
93
        $this->siteConfiguration = $siteConfiguration;
94
    }
95
96
    /**
97
     * Summary of validateName
98
     *
99
     * @param Request $request
100
     *
101
     * @return ValidationError[]
102
     */
103
    public function validateName(Request $request)
104
    {
105
        $errorList = array();
106
107
        // ERRORS
108
        // name is empty
109
        if (trim($request->getName()) == "") {
110
            $errorList[ValidationError::NAME_EMPTY] = new ValidationError(ValidationError::NAME_EMPTY);
111
        }
112
113
        // name is too long
114
        if (mb_strlen(trim($request->getName())) > 500) {
115
            $errorList[ValidationError::NAME_EMPTY] = new ValidationError(ValidationError::NAME_TOO_LONG);
116
        }
117
118
        // username already exists
119
        if ($this->userExists($request)) {
120
            $errorList[ValidationError::NAME_EXISTS] = new ValidationError(ValidationError::NAME_EXISTS);
121
        }
122
123
        // username part of SUL account
124
        if ($this->userSulExists($request)) {
125
            // using same error slot as name exists - it's the same sort of error, and we probably only want to show one.
126
            $errorList[ValidationError::NAME_EXISTS] = new ValidationError(ValidationError::NAME_EXISTS_SUL);
127
        }
128
129
        // username is numbers
130
        if (preg_match("/^[0-9]+$/", $request->getName()) === 1) {
131
            $errorList[ValidationError::NAME_NUMONLY] = new ValidationError(ValidationError::NAME_NUMONLY);
132
        }
133
134
        // username can't contain #@/<>[]|{}
135
        if (preg_match("/[" . preg_quote("#@/<>[]|{}", "/") . "]/", $request->getName()) === 1) {
136
            $errorList[ValidationError::NAME_INVALIDCHAR] = new ValidationError(ValidationError::NAME_INVALIDCHAR);
137
        }
138
139
        // username is an IP
140
        if (filter_var($request->getName(), FILTER_VALIDATE_IP)) {
141
            $errorList[ValidationError::NAME_IP] = new ValidationError(ValidationError::NAME_IP);
142
        }
143
144
        // existing non-closed request for this name
145
        if ($this->nameRequestExists($request)) {
146
            $errorList[ValidationError::OPEN_REQUEST_NAME] = new ValidationError(ValidationError::OPEN_REQUEST_NAME);
147
        }
148
149
        return $errorList;
150
    }
151
152
    /**
153
     * Summary of validateEmail
154
     *
155
     * @param Request $request
156
     * @param string  $emailConfirmation
157
     *
158
     * @return ValidationError[]
159
     */
160
    public function validateEmail(Request $request, $emailConfirmation)
161
    {
162
        $errorList = array();
163
164
        // ERRORS
165
166
        // email addresses must match
167
        if ($request->getEmail() != $emailConfirmation) {
168
            $errorList[ValidationError::EMAIL_MISMATCH] = new ValidationError(ValidationError::EMAIL_MISMATCH);
169
        }
170
171
        // email address must be validly formed
172
        if (trim($request->getEmail()) == "") {
173
            $errorList[ValidationError::EMAIL_EMPTY] = new ValidationError(ValidationError::EMAIL_EMPTY);
174
        }
175
176
        // email address must be validly formed
177
        if (!filter_var($request->getEmail(), FILTER_VALIDATE_EMAIL)) {
178
            if (trim($request->getEmail()) != "") {
179
                $errorList[ValidationError::EMAIL_INVALID] = new ValidationError(ValidationError::EMAIL_INVALID);
180
            }
181
        }
182
183
        // email address can't be wikimedia/wikipedia .com/org
184
        if (preg_match('/.*@.*wiki(m.dia|p.dia)\.(org|com)/i', $request->getEmail()) === 1) {
185
            $errorList[ValidationError::EMAIL_WIKIMEDIA] = new ValidationError(ValidationError::EMAIL_WIKIMEDIA);
186
        }
187
188
        return $errorList;
189
    }
190
191
    /**
192
     * Summary of validateOther
193
     *
194
     * @param Request $request
195
     *
196
     * @return ValidationError[]
197
     */
198
    public function validateOther(Request $request)
199
    {
200
        $errorList = array();
201
202
        $trustedIp = $this->xffTrustProvider->getTrustedClientIp($request->getIp(),
203
            $request->getForwardedIp());
204
205
        // ERRORS
206
207
        // TOR nodes
208
        if ($this->torExitProvider->isTorExit($trustedIp)) {
209
            $errorList[ValidationError::BANNED] = new ValidationError(ValidationError::BANNED_TOR);
210
        }
211
212
        // Bans
213
        if ($this->banHelper->isBlockBanned($request)) {
214
            $errorList[ValidationError::BANNED] = new ValidationError(ValidationError::BANNED);
215
        }
216
217
        return $errorList;
218
    }
219
220
    public function postSaveValidations(Request $request)
221
    {
222
        // Antispoof check
223
        $this->checkAntiSpoof($request);
224
225
        // Blacklist check
226
        $this->checkTitleBlacklist($request);
227
228
        // Add comment for form override
229
        $this->formOverride($request);
230
231
        $bans = $this->banHelper->getBans($request);
232
233
        foreach ($bans as $ban) {
234
            if ($ban->getAction() == Ban::ACTION_DROP) {
235
                $request->setStatus(RequestStatus::CLOSED);
236
                $request->save();
237
238
                Logger::closeRequest($request->getDatabase(), $request, 0, null);
239
240
                $comment = new Comment();
241
                $comment->setDatabase($this->database);
242
                $comment->setRequest($request->getId());
243
                $comment->setVisibility('user');
244
                $comment->setUser(null);
245
246
                $comment->setComment('Request dropped automatically due to matching rule.');
247
                $comment->save();
248
            }
249
250
            if ($ban->getAction() == Ban::ACTION_DEFER) {
251
                /** @var RequestQueue|false $targetQueue */
252
                $targetQueue = RequestQueue::getById($ban->getTargetQueue(), $this->database);
253
254
                if ($targetQueue === false ) {
255
                    $comment = new Comment();
256
                    $comment->setDatabase($this->database);
257
                    $comment->setRequest($request->getId());
258
                    $comment->setVisibility('user');
259
                    $comment->setUser(null);
260
261
                    $comment->setComment("This request would have been deferred automatically due to a matching rule, but the queue to defer to could not be found.");
262
                    $comment->save();
263
                }
264
                else {
265
                    $this->deferRequest($request, $targetQueue, 'Request deferred automatically due to matching rule.');
266
                }
267
            }
268
        }
269
    }
270
271
    private function checkAntiSpoof(Request $request)
272
    {
273
        try {
274
            if (count($this->antiSpoofProvider->getSpoofs($request->getName())) > 0) {
275
                // If there were spoofs an Admin should handle the request.
276
                // FIXME: domains!
277
                $defaultQueue = RequestQueue::getDefaultQueue($this->database, 1, RequestQueue::DEFAULT_ANTISPOOF);
278
                $this->deferRequest($request, $defaultQueue,
0 ignored issues
show
Bug introduced by
It seems like $defaultQueue can also be of type false; however, parameter $targetQueue of Waca\Validation\RequestV...nHelper::deferRequest() does only seem to accept Waca\DataObjects\RequestQueue, 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

278
                $this->deferRequest($request, /** @scrutinizer ignore-type */ $defaultQueue,
Loading history...
279
                    'Request automatically deferred due to AntiSpoof hit');
280
            }
281
        }
282
        catch (Exception $ex) {
283
            $skippable = [
284
                'Encountered error while getting result: Contains unassigned character',
285
                'Encountered error while getting result: Contains incompatible mixed scripts',
286
                'Encountered error while getting result: Does not contain any letters'
287
            ];
288
289
            $skip = false;
290
291
            foreach ($skippable as $s) {
292
                if (strpos($ex->getMessage(), $s) !== false) {
293
                    $skip = true;
294
                    break;
295
                }
296
            }
297
298
            // Only log to disk if this *isn't* a "skippable" error.
299
            if (!$skip) {
300
                ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
301
            }
302
        }
303
    }
304
305
    private function checkTitleBlacklist(Request $request)
306
    {
307
        if ($this->titleBlacklistEnabled == 1) {
308
            try {
309
                $apiResult = $this->httpHelper->get(
310
                    $this->mediawikiApiEndpoint,
311
                    array(
312
                        'action'       => 'titleblacklist',
313
                        'tbtitle'      => $request->getName(),
314
                        'tbaction'     => 'new-account',
315
                        'tbnooverride' => true,
316
                        'format'       => 'php',
317
                    ),
318
                    [],
319
                    $this->validationRemoteTimeout
320
                );
321
322
                $data = unserialize($apiResult);
323
324
                $requestIsOk = $data['titleblacklist']['result'] == "ok";
325
            }
326
            catch (CurlException $ex) {
327
                ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
328
329
                // Don't kill the request, just assume it's fine. Humans can deal with it later.
330
                return;
331
            }
332
333
            if (!$requestIsOk) {
334
                // FIXME: domains!
335
                $defaultQueue = RequestQueue::getDefaultQueue($this->database, 1, RequestQueue::DEFAULT_TITLEBLACKLIST);
336
337
                $this->deferRequest($request, $defaultQueue,
0 ignored issues
show
Bug introduced by
It seems like $defaultQueue can also be of type false; however, parameter $targetQueue of Waca\Validation\RequestV...nHelper::deferRequest() does only seem to accept Waca\DataObjects\RequestQueue, 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

337
                $this->deferRequest($request, /** @scrutinizer ignore-type */ $defaultQueue,
Loading history...
338
                    'Request automatically deferred due to title blacklist hit');
339
            }
340
        }
341
    }
342
343
    private function userExists(Request $request)
344
    {
345
        try {
346
            $userExists = $this->httpHelper->get(
347
                $this->mediawikiApiEndpoint,
348
                array(
349
                    'action'  => 'query',
350
                    'list'    => 'users',
351
                    'ususers' => $request->getName(),
352
                    'format'  => 'php',
353
                ),
354
                [],
355
                $this->validationRemoteTimeout
356
            );
357
358
            $ue = unserialize($userExists);
359
            if (!isset ($ue['query']['users']['0']['missing']) && isset ($ue['query']['users']['0']['userid'])) {
360
                return true;
361
            }
362
        }
363
        catch (CurlException $ex) {
364
            ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
365
366
            // Don't kill the request, just assume it's fine. Humans can deal with it later.
367
            return false;
368
        }
369
370
        return false;
371
    }
372
373
    private function userSulExists(Request $request)
374
    {
375
        $requestName = $request->getName();
376
377
        try {
378
            $userExists = $this->httpHelper->get(
379
                $this->mediawikiApiEndpoint,
380
                array(
381
                    'action'  => 'query',
382
                    'meta'    => 'globaluserinfo',
383
                    'guiuser' => $requestName,
384
                    'format'  => 'php',
385
                ),
386
                [],
387
                $this->validationRemoteTimeout
388
            );
389
390
            $ue = unserialize($userExists);
391
            if (isset ($ue['query']['globaluserinfo']['id'])) {
392
                return true;
393
            }
394
        }
395
        catch (CurlException $ex) {
396
            ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
397
398
            // Don't kill the request, just assume it's fine. Humans can deal with it later.
399
            return false;
400
        }
401
402
        return false;
403
    }
404
405
    /**
406
     * Checks if a request with this name is currently open
407
     *
408
     * @param Request $request
409
     *
410
     * @return bool
411
     */
412
    private function nameRequestExists(Request $request)
413
    {
414
        $query = "SELECT COUNT(id) FROM request WHERE status != 'Closed' AND name = :name;";
415
        $statement = $this->database->prepare($query);
416
        $statement->execute(array(':name' => $request->getName()));
417
418
        if (!$statement) {
0 ignored issues
show
introduced by
$statement is of type PDOStatement, thus it always evaluated to true.
Loading history...
419
            return false;
420
        }
421
422
        return $statement->fetchColumn() > 0;
423
    }
424
425
    private function deferRequest(Request $request, RequestQueue $targetQueue, $deferComment): void
426
    {
427
        $request->setQueue($targetQueue->getId());
428
        $request->save();
429
430
        $logTarget = $targetQueue->getLogName();
0 ignored issues
show
Deprecated Code introduced by
The function Waca\DataObjects\RequestQueue::getLogName() has been deprecated. ( Ignorable by Annotation )

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

430
        $logTarget = /** @scrutinizer ignore-deprecated */ $targetQueue->getLogName();
Loading history...
431
432
        Logger::deferRequest($this->database, $request, $logTarget);
433
434
        $comment = new Comment();
435
        $comment->setDatabase($this->database);
436
        $comment->setRequest($request->getId());
437
        $comment->setVisibility('user');
438
        $comment->setUser(null);
439
440
        $comment->setComment($deferComment);
441
        $comment->save();
442
    }
443
444
    private function formOverride(Request $request)
445
    {
446
        $form = $request->getOriginFormObject();
447
        if($form === null || $form->getOverrideQueue() === null) {
448
            return;
449
        }
450
451
        /** @var RequestQueue $targetQueue */
452
        $targetQueue = RequestQueue::getById($form->getOverrideQueue(), $request->getDatabase());
453
454
        $this->deferRequest($request, $targetQueue, 'Request deferred automatically due to request submission through a request form with a default queue set.');
455
    }
456
}
457