Passed
Push — multiproject/requestqueue ( 1e0fc1...a1c474 )
by Simon
02:57
created

RequestValidationHelper::validateOther()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 20
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 8
dl 0
loc 20
ccs 0
cts 9
cp 0
rs 10
c 1
b 0
f 0
cc 3
nc 4
nop 1
crap 12
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\ExceptionHandler;
16
use Waca\Exceptions\CurlException;
17
use Waca\Helpers\HttpHelper;
18
use Waca\Helpers\Interfaces\IBanHelper;
19
use Waca\Helpers\Logger;
20
use Waca\PdoDatabase;
21
use Waca\Providers\Interfaces\IAntiSpoofProvider;
22
use Waca\Providers\Interfaces\IXffTrustProvider;
23
use Waca\Providers\TorExitProvider;
24
use Waca\RequestStatus;
25
use Waca\SiteConfiguration;
26
27
/**
28
 * Performs the validation of an incoming request.
29
 */
30
class RequestValidationHelper
31
{
32
    /** @var IBanHelper */
33
    private $banHelper;
34
    /** @var PdoDatabase */
35
    private $database;
36
    /** @var IAntiSpoofProvider */
37
    private $antiSpoofProvider;
38
    /** @var IXffTrustProvider */
39
    private $xffTrustProvider;
40
    /** @var HttpHelper */
41
    private $httpHelper;
42
    /**
43
     * @var string
44
     */
45
    private $mediawikiApiEndpoint;
46
    private $titleBlacklistEnabled;
47
    /**
48
     * @var TorExitProvider
49
     */
50
    private $torExitProvider;
51
    /**
52
     * @var SiteConfiguration
53
     */
54
    private $siteConfiguration;
55
56
    private $validationRemoteTimeout = 5000;
57
58
    /**
59
     * Summary of __construct
60
     *
61
     * @param IBanHelper         $banHelper
62
     * @param PdoDatabase        $database
63
     * @param IAntiSpoofProvider $antiSpoofProvider
64
     * @param IXffTrustProvider  $xffTrustProvider
65
     * @param HttpHelper         $httpHelper
66
     * @param TorExitProvider    $torExitProvider
67
     * @param SiteConfiguration  $siteConfiguration
68
     */
69 1
    public function __construct(
70
        IBanHelper $banHelper,
71
        PdoDatabase $database,
72
        IAntiSpoofProvider $antiSpoofProvider,
73
        IXffTrustProvider $xffTrustProvider,
74
        HttpHelper $httpHelper,
75
        TorExitProvider $torExitProvider,
76
        SiteConfiguration $siteConfiguration
77
    ) {
78 1
        $this->banHelper = $banHelper;
79 1
        $this->database = $database;
80 1
        $this->antiSpoofProvider = $antiSpoofProvider;
81 1
        $this->xffTrustProvider = $xffTrustProvider;
82 1
        $this->httpHelper = $httpHelper;
83 1
        $this->mediawikiApiEndpoint = $siteConfiguration->getMediawikiWebServiceEndpoint();
84 1
        $this->titleBlacklistEnabled = $siteConfiguration->getTitleBlacklistEnabled();
85 1
        $this->torExitProvider = $torExitProvider;
86 1
        $this->siteConfiguration = $siteConfiguration;
87 1
    }
88
89
    /**
90
     * Summary of validateName
91
     *
92
     * @param Request $request
93
     *
94
     * @return ValidationError[]
95
     */
96 1
    public function validateName(Request $request)
97
    {
98 1
        $errorList = array();
99
100
        // ERRORS
101
        // name is empty
102 1
        if (trim($request->getName()) == "") {
103
            $errorList[ValidationError::NAME_EMPTY] = new ValidationError(ValidationError::NAME_EMPTY);
104
        }
105
106
        // username already exists
107 1
        if ($this->userExists($request)) {
108
            $errorList[ValidationError::NAME_EXISTS] = new ValidationError(ValidationError::NAME_EXISTS);
109
        }
110
111
        // username part of SUL account
112 1
        if ($this->userSulExists($request)) {
113
            // using same error slot as name exists - it's the same sort of error, and we probably only want to show one.
114
            $errorList[ValidationError::NAME_EXISTS] = new ValidationError(ValidationError::NAME_EXISTS_SUL);
115
        }
116
117
        // username is numbers
118 1
        if (preg_match("/^[0-9]+$/", $request->getName()) === 1) {
119
            $errorList[ValidationError::NAME_NUMONLY] = new ValidationError(ValidationError::NAME_NUMONLY);
120
        }
121
122
        // username can't contain #@/<>[]|{}
123 1
        if (preg_match("/[" . preg_quote("#@/<>[]|{}", "/") . "]/", $request->getName()) === 1) {
124
            $errorList[ValidationError::NAME_INVALIDCHAR] = new ValidationError(ValidationError::NAME_INVALIDCHAR);
125
        }
126
        
127
        // username is an IP
128 1
        if (filter_var($request->getName(), FILTER_VALIDATE_IP)) {
129
            $errorList[ValidationError::NAME_IP] = new ValidationError(ValidationError::NAME_IP);
130
        }
131
132
        // existing non-closed request for this name
133 1
        if ($this->nameRequestExists($request)) {
134
            $errorList[ValidationError::OPEN_REQUEST_NAME] = new ValidationError(ValidationError::OPEN_REQUEST_NAME);
135
        }
136
137 1
        return $errorList;
138
    }
139
140
    /**
141
     * Summary of validateEmail
142
     *
143
     * @param Request $request
144
     * @param string  $emailConfirmation
145
     *
146
     * @return ValidationError[]
147
     */
148
    public function validateEmail(Request $request, $emailConfirmation)
149
    {
150
        $errorList = array();
151
152
        // ERRORS
153
154
        // email addresses must match
155
        if ($request->getEmail() != $emailConfirmation) {
156
            $errorList[ValidationError::EMAIL_MISMATCH] = new ValidationError(ValidationError::EMAIL_MISMATCH);
157
        }
158
159
        // email address must be validly formed
160
        if (trim($request->getEmail()) == "") {
161
            $errorList[ValidationError::EMAIL_EMPTY] = new ValidationError(ValidationError::EMAIL_EMPTY);
162
        }
163
164
        // email address must be validly formed
165
        if (!filter_var($request->getEmail(), FILTER_VALIDATE_EMAIL)) {
166
            if (trim($request->getEmail()) != "") {
167
                $errorList[ValidationError::EMAIL_INVALID] = new ValidationError(ValidationError::EMAIL_INVALID);
168
            }
169
        }
170
171
        // email address can't be wikimedia/wikipedia .com/org
172
        if (preg_match('/.*@.*wiki(m.dia|p.dia)\.(org|com)/i', $request->getEmail()) === 1) {
173
            $errorList[ValidationError::EMAIL_WIKIMEDIA] = new ValidationError(ValidationError::EMAIL_WIKIMEDIA);
174
        }
175
176
        return $errorList;
177
    }
178
179
    /**
180
     * Summary of validateOther
181
     *
182
     * @param Request $request
183
     *
184
     * @return ValidationError[]
185
     */
186
    public function validateOther(Request $request)
187
    {
188
        $errorList = array();
189
190
        $trustedIp = $this->xffTrustProvider->getTrustedClientIp($request->getIp(),
191
            $request->getForwardedIp());
192
193
        // ERRORS
194
195
        // TOR nodes
196
        if ($this->torExitProvider->isTorExit($trustedIp)) {
197
            $errorList[ValidationError::BANNED] = new ValidationError(ValidationError::BANNED_TOR);
198
        }
199
200
        // Bans
201
        if ($this->banHelper->isBlockBanned($request)) {
202
            $errorList[ValidationError::BANNED] = new ValidationError(ValidationError::BANNED);
203
        }
204
205
        return $errorList;
206
    }
207
208
    public function postSaveValidations(Request $request)
209
    {
210
        // Antispoof check
211
        $this->checkAntiSpoof($request);
212
213
        // Blacklist check
214
        $this->checkTitleBlacklist($request);
215
216
        $bans = $this->banHelper->getBans($request);
217
218
        foreach ($bans as $ban) {
219
            if ($ban->getAction() == Ban::ACTION_DROP) {
220
                $request->setStatus(RequestStatus::CLOSED);
221
                $request->save();
222
223
                Logger::closeRequest($request->getDatabase(), $request, 0, null);
224
225
                $comment = new Comment();
226
                $comment->setDatabase($this->database);
227
                $comment->setRequest($request->getId());
228
                $comment->setVisibility('user');
229
                $comment->setUser(null);
230
231
                $comment->setComment('Request dropped automatically due to matching rule.');
232
                $comment->save();
233
            }
234
235
            if ($ban->getAction() == Ban::ACTION_DEFER) {
236
                $this->deferRequest($request, $ban->getActionTarget(), 'Request deferred automatically due to matching rule.');
237
            }
238
        }
239
    }
240
241
    private function checkAntiSpoof(Request $request)
242
    {
243
        try {
244
            if (count($this->antiSpoofProvider->getSpoofs($request->getName())) > 0) {
245
                // If there were spoofs an Admin should handle the request.
246
                $this->deferRequest($request, 'Flagged users',
247
                    'Request automatically deferred to flagged users due to AntiSpoof hit');
248
            }
249
        }
250
        catch (Exception $ex) {
251
            ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
252
        }
253
    }
254
255
    private function checkTitleBlacklist(Request $request)
256
    {
257
        if ($this->titleBlacklistEnabled == 1) {
258
            try {
259
                $apiResult = $this->httpHelper->get(
260
                    $this->mediawikiApiEndpoint,
261
                    array(
262
                        'action'       => 'titleblacklist',
263
                        'tbtitle'      => $request->getName(),
264
                        'tbaction'     => 'new-account',
265
                        'tbnooverride' => true,
266
                        'format'       => 'php',
267
                    ),
268
                    [],
269
                    $this->validationRemoteTimeout
270
                );
271
272
                $data = unserialize($apiResult);
273
274
                $requestIsOk = $data['titleblacklist']['result'] == "ok";
275
            }
276
            catch (CurlException $ex) {
277
                ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
278
279
                // Don't kill the request, just assume it's fine. Humans can deal with it later.
280
                return;
281
            }
282
283
            if (!$requestIsOk) {
284
                $this->deferRequest($request, 'Flagged users',
285
                    'Request automatically deferred to flagged users due to title blacklist hit');
286
            }
287
        }
288
    }
289
290 1
    private function userExists(Request $request)
291
    {
292
        try {
293 1
            $userExists = $this->httpHelper->get(
294 1
                $this->mediawikiApiEndpoint,
295
                array(
296 1
                    'action'  => 'query',
297 1
                    'list'    => 'users',
298 1
                    'ususers' => $request->getName(),
299 1
                    'format'  => 'php',
300
                ),
301 1
                [],
302 1
                $this->validationRemoteTimeout
303
            );
304
305 1
            $ue = unserialize($userExists);
306 1
            if (!isset ($ue['query']['users']['0']['missing']) && isset ($ue['query']['users']['0']['userid'])) {
307 1
                return true;
308
            }
309
        }
310
        catch (CurlException $ex) {
311
            ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
312
313
            // Don't kill the request, just assume it's fine. Humans can deal with it later.
314
            return false;
315
        }
316
317 1
        return false;
318
    }
319
320 1
    private function userSulExists(Request $request)
321
    {
322 1
        $requestName = $request->getName();
323
324
        try {
325 1
            $userExists = $this->httpHelper->get(
326 1
                $this->mediawikiApiEndpoint,
327
                array(
328 1
                    'action'  => 'query',
329 1
                    'meta'    => 'globaluserinfo',
330 1
                    'guiuser' => $requestName,
331 1
                    'format'  => 'php',
332
                ),
333 1
                [],
334 1
                $this->validationRemoteTimeout
335
            );
336
337 1
            $ue = unserialize($userExists);
338 1
            if (isset ($ue['query']['globaluserinfo']['id'])) {
339 1
                return true;
340
            }
341
        }
342
        catch (CurlException $ex) {
343
            ExceptionHandler::logExceptionToDisk($ex, $this->siteConfiguration);
344
345
            // Don't kill the request, just assume it's fine. Humans can deal with it later.
346
            return false;
347
        }
348
349 1
        return false;
350
    }
351
352
    /**
353
     * Checks if a request with this name is currently open
354
     *
355
     * @param Request $request
356
     *
357
     * @return bool
358
     */
359 1
    private function nameRequestExists(Request $request)
360
    {
361 1
        $query = "SELECT COUNT(id) FROM request WHERE status != 'Closed' AND name = :name;";
362 1
        $statement = $this->database->prepare($query);
363 1
        $statement->execute(array(':name' => $request->getName()));
364
365 1
        if (!$statement) {
0 ignored issues
show
introduced by
$statement is of type PDOStatement, thus it always evaluated to true.
Loading history...
366
            return false;
367
        }
368
369 1
        return $statement->fetchColumn() > 0;
370
    }
371
372
    private function deferRequest(Request $request, $targetQueue, $deferComment): void
373
    {
374
        $request->setStatus($targetQueue);
375
        $request->save();
376
377
        $logTarget = $this->siteConfiguration->getRequestStates()[$targetQueue]['defertolog'];
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

377
        $logTarget = /** @scrutinizer ignore-deprecated */ $this->siteConfiguration->getRequestStates()[$targetQueue]['defertolog'];

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...
378
379
        Logger::deferRequest($this->database, $request, $logTarget);
380
381
        $comment = new Comment();
382
        $comment->setDatabase($this->database);
383
        $comment->setRequest($request->getId());
384
        $comment->setVisibility('user');
385
        $comment->setUser(null);
386
387
        $comment->setComment($deferComment);
388
        $comment->save();
389
    }
390
}
391