Passed
Push — comment-flagging ( 6669bb...9d978e )
by Simon
06:56 queued 02:53
created

RequestValidationHelper::validateEmail()   A

Complexity

Conditions 6
Paths 24

Size

Total Lines 29
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

243
                $this->deferRequest($request, /** @scrutinizer ignore-type */ $targetQueue, 'Request deferred automatically due to matching rule.');
Loading history...
244
            }
245
        }
246
    }
247
248
    private function checkAntiSpoof(Request $request)
249
    {
250
        try {
251
            if (count($this->antiSpoofProvider->getSpoofs($request->getName())) > 0) {
252
                // If there were spoofs an Admin should handle the request.
253
                // FIXME: domains!
254
                $defaultQueue = RequestQueue::getDefaultQueue($this->database, 1, RequestQueue::DEFAULT_ANTISPOOF);
255
                $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

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

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

407
        $logTarget = /** @scrutinizer ignore-deprecated */ $targetQueue->getLogName();
Loading history...
408
409
        Logger::deferRequest($this->database, $request, $logTarget);
410
411
        $comment = new Comment();
412
        $comment->setDatabase($this->database);
413
        $comment->setRequest($request->getId());
414
        $comment->setVisibility('user');
415
        $comment->setUser(null);
416
417
        $comment->setComment($deferComment);
418
        $comment->save();
419
    }
420
}
421