Failed Conditions
Pull Request — bugsquish (#574)
by Simon
05:46 queued 03:15
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 1
Bugs 1 Features 0
Metric Value
eloc 11
dl 0
loc 29
ccs 0
cts 18
cp 0
rs 9.2222
c 1
b 1
f 0
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\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
        // existing non-closed request for this name
124
        if ($this->nameRequestExists($request)) {
125
            $errorList[ValidationError::OPEN_REQUEST_NAME] = new ValidationError(ValidationError::OPEN_REQUEST_NAME);
126
        }
127
128
        return $errorList;
129
    }
130
131
    /**
132
     * Summary of validateEmail
133
     *
134
     * @param Request $request
135
     * @param string  $emailConfirmation
136
     *
137
     * @return ValidationError[]
138
     */
139
    public function validateEmail(Request $request, $emailConfirmation)
140
    {
141
        $errorList = array();
142
143
        // ERRORS
144
145
        // email addresses must match
146
        if ($request->getEmail() != $emailConfirmation) {
147
            $errorList[ValidationError::EMAIL_MISMATCH] = new ValidationError(ValidationError::EMAIL_MISMATCH);
148
        }
149
150
        // email address must be validly formed
151
        if (trim($request->getEmail()) == "") {
152
            $errorList[ValidationError::EMAIL_EMPTY] = new ValidationError(ValidationError::EMAIL_EMPTY);
153
        }
154
155
        // email address must be validly formed
156
        if (!filter_var($request->getEmail(), FILTER_VALIDATE_EMAIL)) {
157
            if (trim($request->getEmail()) != "") {
158
                $errorList[ValidationError::EMAIL_INVALID] = new ValidationError(ValidationError::EMAIL_INVALID);
159
            }
160
        }
161
162
        // email address can't be wikimedia/wikipedia .com/org
163
        if (preg_match('/.*@.*wiki(m.dia|p.dia)\.(org|com)/i', $request->getEmail()) === 1) {
164
            $errorList[ValidationError::EMAIL_WIKIMEDIA] = new ValidationError(ValidationError::EMAIL_WIKIMEDIA);
165
        }
166
167
        return $errorList;
168
    }
169
170
    /**
171
     * Summary of validateOther
172
     *
173
     * @param Request $request
174
     *
175
     * @return ValidationError[]
176
     */
177
    public function validateOther(Request $request)
178
    {
179
        $errorList = array();
180
181
        $trustedIp = $this->xffTrustProvider->getTrustedClientIp($request->getIp(),
182
            $request->getForwardedIp());
183
184
        // ERRORS
185
186
        // TOR nodes
187
        if ($this->torExitProvider->isTorExit($trustedIp)) {
188
            $errorList[ValidationError::BANNED] = new ValidationError(ValidationError::BANNED_TOR);
189
        }
190
191
        // Bans
192
        if ($this->banHelper->isBlockBanned($request)) {
193
            $errorList[ValidationError::BANNED] = new ValidationError(ValidationError::BANNED);
194
        }
195
196
        return $errorList;
197
    }
198
199
    public function postSaveValidations(Request $request)
200
    {
201
        // Antispoof check
202
        $this->checkAntiSpoof($request);
203
204
        // Blacklist check
205
        $this->checkTitleBlacklist($request);
206
207
        $bans = $this->banHelper->getBans($request);
208
209
        foreach ($bans as $ban) {
210
            if ($ban->getAction() == Ban::ACTION_DROP) {
211
                $request->setStatus(RequestStatus::CLOSED);
212
                $request->save();
213
214
                Logger::closeRequest($request->getDatabase(), $request, 0, null);
215
216
                $comment = new Comment();
217
                $comment->setDatabase($this->database);
218
                $comment->setRequest($request->getId());
219
                $comment->setVisibility('user');
220
                $comment->setUser(null);
221
222
                $comment->setComment('Request dropped automatically due to matching rule.');
223
                $comment->save();
224
            }
225
226
            if ($ban->getAction() == Ban::ACTION_DEFER) {
227
                $this->deferRequest($request, $ban->getActionTarget(), 'Request deferred automatically due to matching rule.');
228
            }
229
        }
230
    }
231
232
    private function checkAntiSpoof(Request $request)
233
    {
234
        try {
235
            if (count($this->antiSpoofProvider->getSpoofs($request->getName())) > 0) {
236
                // If there were spoofs an Admin should handle the request.
237
                $this->deferRequest($request, 'Flagged users',
238
                    'Request automatically deferred to flagged users due to AntiSpoof hit');
239
            }
240
        }
241
        catch (Exception $ex) {
242
            // logme
243
        }
244
    }
245
246
    private function checkTitleBlacklist(Request $request)
247
    {
248
        if ($this->titleBlacklistEnabled == 1) {
249
            $apiResult = $this->httpHelper->get(
250
                $this->mediawikiApiEndpoint,
251
                array(
252
                    'action'       => 'titleblacklist',
253
                    'tbtitle'      => $request->getName(),
254
                    'tbaction'     => 'new-account',
255
                    'tbnooverride' => true,
256
                    'format'       => 'php',
257
                )
258
            );
259
260
            $data = unserialize($apiResult);
261
262
            $requestIsOk = $data['titleblacklist']['result'] == "ok";
263
264
            if (!$requestIsOk) {
265
                $this->deferRequest($request, 'Flagged users',
266
                    'Request automatically deferred to flagged users due to title blacklist hit');
267
            }
268
        }
269
    }
270
271
    private function userExists(Request $request)
272
    {
273
        $userExists = $this->httpHelper->get(
274
            $this->mediawikiApiEndpoint,
275
            array(
276
                'action'  => 'query',
277
                'list'    => 'users',
278
                'ususers' => $request->getName(),
279
                'format'  => 'php',
280
            )
281
        );
282
283
        $ue = unserialize($userExists);
284
        if (!isset ($ue['query']['users']['0']['missing']) && isset ($ue['query']['users']['0']['userid'])) {
285
            return true;
286
        }
287
288
        return false;
289
    }
290
291
    private function userSulExists(Request $request)
292
    {
293
        $requestName = $request->getName();
294
295
        $userExists = $this->httpHelper->get(
296
            $this->mediawikiApiEndpoint,
297
            array(
298
                'action'  => 'query',
299
                'meta'    => 'globaluserinfo',
300
                'guiuser' => $requestName,
301
                'format'  => 'php',
302
            )
303
        );
304
305
        $ue = unserialize($userExists);
306
        if (isset ($ue['query']['globaluserinfo']['id'])) {
307
            return true;
308
        }
309
310
        return false;
311
    }
312
313
    /**
314
     * Checks if a request with this name is currently open
315
     *
316
     * @param Request $request
317
     *
318
     * @return bool
319
     */
320
    private function nameRequestExists(Request $request)
321
    {
322
        $query = "SELECT COUNT(id) FROM request WHERE status != 'Closed' AND name = :name;";
323
        $statement = $this->database->prepare($query);
324
        $statement->execute(array(':name' => $request->getName()));
325
326
        if (!$statement) {
327
            return false;
328
        }
329
330
        return $statement->fetchColumn() > 0;
331
    }
332
333
    private function deferRequest(Request $request, $targetQueue, $deferComment): void
334
    {
335
        $request->setStatus($targetQueue);
336
        $request->save();
337
338
        $logTarget = $this->siteConfiguration->getRequestStates()[$targetQueue]['defertolog'];
339
340
        Logger::deferRequest($this->database, $request, $logTarget);
341
342
        $comment = new Comment();
343
        $comment->setDatabase($this->database);
344
        $comment->setRequest($request->getId());
345
        $comment->setVisibility('user');
346
        $comment->setUser(null);
347
348
        $comment->setComment($deferComment);
349
        $comment->save();
350
    }
351
}
352