Failed Conditions
Pull Request — multiproject/requestqueue (#704)
by Simon
02:29
created

RequestValidationHelper::userSulExists()   A

Complexity

Conditions 3
Paths 5

Size

Total Lines 30
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 3.0494

Importance

Changes 4
Bugs 1 Features 0
Metric Value
eloc 17
dl 0
loc 30
ccs 14
cts 17
cp 0.8235
rs 9.7
c 4
b 1
f 0
cc 3
nc 5
nop 1
crap 3.0494
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
        // username already exists
108 1
        if ($this->userExists($request)) {
109
            $errorList[ValidationError::NAME_EXISTS] = new ValidationError(ValidationError::NAME_EXISTS);
110
        }
111
112
        // username part of SUL account
113 1
        if ($this->userSulExists($request)) {
114
            // using same error slot as name exists - it's the same sort of error, and we probably only want to show one.
115
            $errorList[ValidationError::NAME_EXISTS] = new ValidationError(ValidationError::NAME_EXISTS_SUL);
116
        }
117
118
        // username is numbers
119 1
        if (preg_match("/^[0-9]+$/", $request->getName()) === 1) {
120
            $errorList[ValidationError::NAME_NUMONLY] = new ValidationError(ValidationError::NAME_NUMONLY);
121
        }
122
123
        // username can't contain #@/<>[]|{}
124 1
        if (preg_match("/[" . preg_quote("#@/<>[]|{}", "/") . "]/", $request->getName()) === 1) {
125
            $errorList[ValidationError::NAME_INVALIDCHAR] = new ValidationError(ValidationError::NAME_INVALIDCHAR);
126
        }
127
128
        // username is an IP
129 1
        if (filter_var($request->getName(), FILTER_VALIDATE_IP)) {
130
            $errorList[ValidationError::NAME_IP] = new ValidationError(ValidationError::NAME_IP);
131
        }
132
133
        // existing non-closed request for this name
134 1
        if ($this->nameRequestExists($request)) {
135
            $errorList[ValidationError::OPEN_REQUEST_NAME] = new ValidationError(ValidationError::OPEN_REQUEST_NAME);
136
        }
137
138 1
        return $errorList;
139
    }
140
141
    /**
142
     * Summary of validateEmail
143
     *
144
     * @param Request $request
145
     * @param string  $emailConfirmation
146
     *
147
     * @return ValidationError[]
148
     */
149
    public function validateEmail(Request $request, $emailConfirmation)
150
    {
151
        $errorList = array();
152
153
        // ERRORS
154
155
        // email addresses must match
156
        if ($request->getEmail() != $emailConfirmation) {
157
            $errorList[ValidationError::EMAIL_MISMATCH] = new ValidationError(ValidationError::EMAIL_MISMATCH);
158
        }
159
160
        // email address must be validly formed
161
        if (trim($request->getEmail()) == "") {
162
            $errorList[ValidationError::EMAIL_EMPTY] = new ValidationError(ValidationError::EMAIL_EMPTY);
163
        }
164
165
        // email address must be validly formed
166
        if (!filter_var($request->getEmail(), FILTER_VALIDATE_EMAIL)) {
167
            if (trim($request->getEmail()) != "") {
168
                $errorList[ValidationError::EMAIL_INVALID] = new ValidationError(ValidationError::EMAIL_INVALID);
169
            }
170
        }
171
172
        // email address can't be wikimedia/wikipedia .com/org
173
        if (preg_match('/.*@.*wiki(m.dia|p.dia)\.(org|com)/i', $request->getEmail()) === 1) {
174
            $errorList[ValidationError::EMAIL_WIKIMEDIA] = new ValidationError(ValidationError::EMAIL_WIKIMEDIA);
175
        }
176
177
        return $errorList;
178
    }
179
180
    /**
181
     * Summary of validateOther
182
     *
183
     * @param Request $request
184
     *
185
     * @return ValidationError[]
186
     */
187
    public function validateOther(Request $request)
188
    {
189
        $errorList = array();
190
191
        $trustedIp = $this->xffTrustProvider->getTrustedClientIp($request->getIp(),
192
            $request->getForwardedIp());
193
194
        // ERRORS
195
196
        // TOR nodes
197
        if ($this->torExitProvider->isTorExit($trustedIp)) {
198
            $errorList[ValidationError::BANNED] = new ValidationError(ValidationError::BANNED_TOR);
199
        }
200
201
        // Bans
202
        if ($this->banHelper->isBlockBanned($request)) {
203
            $errorList[ValidationError::BANNED] = new ValidationError(ValidationError::BANNED);
204
        }
205
206
        return $errorList;
207
    }
208
209
    public function postSaveValidations(Request $request)
210
    {
211
        // Antispoof check
212
        $this->checkAntiSpoof($request);
213
214
        // Blacklist check
215
        $this->checkTitleBlacklist($request);
216
217
        $bans = $this->banHelper->getBans($request);
218
219
        foreach ($bans as $ban) {
220
            if ($ban->getAction() == Ban::ACTION_DROP) {
221
                $request->setStatus(RequestStatus::CLOSED);
222
                $request->save();
223
224
                Logger::closeRequest($request->getDatabase(), $request, 0, null);
225
226
                $comment = new Comment();
227
                $comment->setDatabase($this->database);
228
                $comment->setRequest($request->getId());
229
                $comment->setVisibility('user');
230
                $comment->setUser(null);
231
232
                $comment->setComment('Request dropped automatically due to matching rule.');
233
                $comment->save();
234
            }
235
236
            if ($ban->getAction() == Ban::ACTION_DEFER) {
237
                $targetQueue = RequestQueue::getById($ban->getTargetQueue(), $this->database);
238
                $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

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

250
                $this->deferRequest($request, /** @scrutinizer ignore-type */ $defaultQueue,
Loading history...
251
                    'Request automatically deferred due to AntiSpoof hit');
252
            }
253
        }
254
        catch (Exception $ex) {
255
            ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
256
        }
257
    }
258
259
    private function checkTitleBlacklist(Request $request)
260
    {
261
        if ($this->titleBlacklistEnabled == 1) {
262
            try {
263
                $apiResult = $this->httpHelper->get(
264
                    $this->mediawikiApiEndpoint,
265
                    array(
266
                        'action'       => 'titleblacklist',
267
                        'tbtitle'      => $request->getName(),
268
                        'tbaction'     => 'new-account',
269
                        'tbnooverride' => true,
270
                        'format'       => 'php',
271
                    ),
272
                    [],
273
                    $this->validationRemoteTimeout
274
                );
275
276
                $data = unserialize($apiResult);
277
278
                $requestIsOk = $data['titleblacklist']['result'] == "ok";
279
            }
280
            catch (CurlException $ex) {
281
                ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
282
283
                // Don't kill the request, just assume it's fine. Humans can deal with it later.
284
                return;
285
            }
286
287
            if (!$requestIsOk) {
288
                // FIXME: domains!
289
                $defaultQueue = RequestQueue::getDefaultQueue($this->database, 1, RequestQueue::DEFAULT_TITLEBLACKLIST);
290
291
                $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

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

384
        $logTarget = /** @scrutinizer ignore-deprecated */ $targetQueue->getLogName();
Loading history...
385
386
        Logger::deferRequest($this->database, $request, $logTarget);
387
388
        $comment = new Comment();
389
        $comment->setDatabase($this->database);
390
        $comment->setRequest($request->getId());
391
        $comment->setVisibility('user');
392
        $comment->setUser(null);
393
394
        $comment->setComment($deferComment);
395
        $comment->save();
396
    }
397
}
398