Passed
Push — search ( 8b187a...5eeca1 )
by Simon
09:13 queued 05:22
created

RequestValidationHelper::validateName()   B

Complexity

Conditions 8
Paths 128

Size

Total Lines 42
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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