Failed Conditions
Push — multiproject/domainsettings ( c67fcc )
by Simon
04:29
created

RequestValidationHelper::validateName()   C

Complexity

Conditions 9
Paths 256

Size

Total Lines 47
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
eloc 18
dl 0
loc 47
ccs 0
cts 28
cp 0
rs 6.5222
c 0
b 0
f 0
cc 9
nc 256
nop 1
crap 90
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
        $bans = $this->banHelper->getBans($request);
229
230
        foreach ($bans as $ban) {
231
            if ($ban->getAction() == Ban::ACTION_DROP) {
232
                $request->setStatus(RequestStatus::CLOSED);
233
                $request->save();
234
235
                Logger::closeRequest($request->getDatabase(), $request, 0, null);
236
237
                $comment = new Comment();
238
                $comment->setDatabase($this->database);
239
                $comment->setRequest($request->getId());
240
                $comment->setVisibility('user');
241
                $comment->setUser(null);
242
243
                $comment->setComment('Request dropped automatically due to matching rule.');
244
                $comment->save();
245
            }
246
247
            if ($ban->getAction() == Ban::ACTION_DEFER) {
248
                /** @var RequestQueue|false $targetQueue */
249
                $targetQueue = RequestQueue::getById($ban->getTargetQueue(), $this->database);
250
251
                if ($targetQueue === false ) {
252
                    $comment = new Comment();
253
                    $comment->setDatabase($this->database);
254
                    $comment->setRequest($request->getId());
255
                    $comment->setVisibility('user');
256
                    $comment->setUser(null);
257
258
                    $comment->setComment("This request would have been deferred automatically due to a matching rule, but the queue to defer to could not be found.");
259
                    $comment->save();
260
                }
261
                else {
262
                    $this->deferRequest($request, $targetQueue, 'Request deferred automatically due to matching rule.');
263
                }
264
            }
265
        }
266
    }
267
268
    private function checkAntiSpoof(Request $request)
269
    {
270
        try {
271
            if (count($this->antiSpoofProvider->getSpoofs($request->getName())) > 0) {
272
                // If there were spoofs an Admin should handle the request.
273
                // FIXME: domains!
274
                $defaultQueue = RequestQueue::getDefaultQueue($this->database, 1, RequestQueue::DEFAULT_ANTISPOOF);
275
                $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

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

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

427
        $logTarget = /** @scrutinizer ignore-deprecated */ $targetQueue->getLogName();
Loading history...
428
429
        Logger::deferRequest($this->database, $request, $logTarget);
430
431
        $comment = new Comment();
432
        $comment->setDatabase($this->database);
433
        $comment->setRequest($request->getId());
434
        $comment->setVisibility('user');
435
        $comment->setUser(null);
436
437
        $comment->setComment($deferComment);
438
        $comment->save();
439
    }
440
}
441