Passed
Push — master ( c3ec55...6fe0c7 )
by Simon
04:07
created

User   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 441
Duplicated Lines 0 %

Test Coverage

Coverage 1.68%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 42
eloc 136
c 2
b 0
f 0
dl 0
loc 441
ccs 2
cts 118
cp 0.0168
rs 9.0399

26 Methods

Rating   Name   Duplication   Size   Complexity  
A getById() 0 10 3
A getByUsername() 0 18 3
A getCurrent() 0 24 4
A getCommunity() 0 7 2
A getByOnWikiUsername() 0 15 2
A getEmail() 0 3 1
A getConfirmationDiff() 0 3 1
A setStatus() 0 3 1
A getForceLogout() 0 3 1
A isActive() 0 3 1
A getLastActive() 0 3 1
A setUsername() 0 7 2
A setOnWikiName() 0 3 1
A getForceIdentified() 0 7 2
A setEmail() 0 3 1
A isSuspended() 0 3 1
B save() 0 66 5
A getOnWikiName() 0 3 1
A getApprovalDate() 0 18 1
A isNewUser() 0 3 1
A getUsername() 0 3 1
A isDeclined() 0 3 1
A setForceLogout() 0 3 2
A isCommunityUser() 0 3 1
A setConfirmationDiff() 0 3 1
A getStatus() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like User often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use User, and based on these observations, apply Extract Interface, too.

1
<?php
2
/******************************************************************************
3
 * Wikipedia Account Creation Assistance tool                                 *
4
 * ACC Development Team. Please see team.json for a list of contributors.     *
5
 *                                                                            *
6
 * This is free and unencumbered software released into the public domain.    *
7
 * Please see LICENSE.md for the full licencing statement.                    *
8
 ******************************************************************************/
9
10
namespace Waca\DataObjects;
11
12
use DateTime;
13
use Exception;
14
use Waca\DataObject;
15
use Waca\Exceptions\OptimisticLockFailedException;
16
use Waca\IIdentificationVerifier;
17
use Waca\PdoDatabase;
18
use Waca\WebRequest;
19
20
/**
21
 * User data object
22
 */
23
class User extends DataObject
24
{
25
    const STATUS_ACTIVE = 'Active';
26
    const STATUS_SUSPENDED = 'Suspended';
27
    const STATUS_DECLINED = 'Declined';
28
    const STATUS_NEW = 'New';
29
30
    private static CommunityUser $community;
31
32
    private $username;
33
    private $email;
34
    private $status = self::STATUS_NEW;
35
    private $onwikiname;
36
    private $lastactive = "0000-00-00 00:00:00";
37
    private $forcelogout = 0;
38
    private $forceidentified = null;
39
    private $confirmationdiff = 0;
40
    /** @var User Cache variable of the current user - it's never going to change in the middle of a request. */
41
    private static $currentUser;
42
    #region Object load methods
43
44
    /**
45
     * Gets the currently logged in user
46
     *
47
     * @param PdoDatabase $database
48
     *
49
     * @return User|CommunityUser
50
     */
51
    public static function getCurrent(PdoDatabase $database)
52
    {
53
        if (self::$currentUser === null) {
54
            $sessionId = WebRequest::getSessionUserId();
55
56
            if ($sessionId !== null) {
57
                /** @var User $user */
58
                $user = self::getById($sessionId, $database);
59
60
                if ($user === false) {
0 ignored issues
show
introduced by
The condition $user === false is always false.
Loading history...
61
                    self::$currentUser = new CommunityUser();
62
                }
63
                else {
64
                    self::$currentUser = $user;
65
                }
66
            }
67
            else {
68
                $anonymousCoward = new CommunityUser();
69
70
                self::$currentUser = $anonymousCoward;
71
            }
72
        }
73
74
        return self::$currentUser;
75
    }
76
77
    /**
78
     * Gets a user by their user ID
79
     *
80
     * Pass -1 to get the community user.
81
     *
82
     * @param int|null    $id
83
     * @param PdoDatabase $database
84
     *
85
     * @return User|false
86
     */
87
    public static function getById($id, PdoDatabase $database)
88
    {
89
        if ($id === null || $id == -1) {
90
            return new CommunityUser();
91
        }
92
93
        /** @var User|false $user */
94
        $user = parent::getById($id, $database);
95
96
        return $user;
97
    }
98
99 2
    public static function getCommunity(): CommunityUser
100
    {
101 2
        if (!isset(self::$community)) {
102
            self::$community = new CommunityUser();
103
        }
104
105
        return self::$community;
106
    }
107
108
    /**
109
     * Gets a user by their username
110
     *
111
     * @param  string      $username
112
     * @param  PdoDatabase $database
113
     *
114
     * @return CommunityUser|User|false
115
     */
116
    public static function getByUsername($username, PdoDatabase $database)
117
    {
118
        if ($username === self::getCommunity()->getUsername()) {
119
            return new CommunityUser();
120
        }
121
122
        $statement = $database->prepare("SELECT * FROM user WHERE username = :id LIMIT 1;");
123
        $statement->bindValue(":id", $username);
124
125
        $statement->execute();
126
127
        $resultObject = $statement->fetchObject(get_called_class());
128
129
        if ($resultObject != false) {
130
            $resultObject->setDatabase($database);
131
        }
132
133
        return $resultObject;
134
    }
135
136
    /**
137
     * Gets a user by their on-wiki username.
138
     *
139
     * @param string      $username
140
     * @param PdoDatabase $database
141
     *
142
     * @return User|false
143
     */
144
    public static function getByOnWikiUsername($username, PdoDatabase $database)
145
    {
146
        $statement = $database->prepare("SELECT * FROM user WHERE onwikiname = :id LIMIT 1;");
147
        $statement->bindValue(":id", $username);
148
        $statement->execute();
149
150
        $resultObject = $statement->fetchObject(get_called_class());
151
152
        if ($resultObject != false) {
153
            $resultObject->setDatabase($database);
154
155
            return $resultObject;
156
        }
157
158
        return false;
159
    }
160
161
    #endregion
162
163
    /**
164
     * Saves the current object
165
     *
166
     * @throws Exception
167
     */
168
    public function save()
169
    {
170
        if ($this->isNew()) {
171
            // insert
172
            $statement = $this->dbObject->prepare(<<<SQL
173
				INSERT INTO `user` ( 
174
					username, email, status, onwikiname, 
175
					lastactive, forcelogout, forceidentified,
176
					confirmationdiff
177
				) VALUES (
178
					:username, :email, :status, :onwikiname,
179
					:lastactive, :forcelogout, null,
180
					:confirmationdiff
181
				);
182
SQL
183
            );
184
            $statement->bindValue(":username", $this->username);
185
            $statement->bindValue(":email", $this->email);
186
            $statement->bindValue(":status", $this->status);
187
            $statement->bindValue(":onwikiname", $this->onwikiname);
188
            $statement->bindValue(":lastactive", $this->lastactive);
189
            $statement->bindValue(":forcelogout", $this->forcelogout);
190
            $statement->bindValue(":confirmationdiff", $this->confirmationdiff);
191
192
            if ($statement->execute()) {
193
                $this->id = (int)$this->dbObject->lastInsertId();
194
            }
195
            else {
196
                throw new Exception($statement->errorInfo());
0 ignored issues
show
Bug introduced by
$statement->errorInfo() of type array is incompatible with the type string expected by parameter $message of Exception::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

196
                throw new Exception(/** @scrutinizer ignore-type */ $statement->errorInfo());
Loading history...
197
            }
198
        }
199
        else {
200
            // update
201
            $statement = $this->dbObject->prepare(<<<SQL
202
				UPDATE `user` SET 
203
					username = :username, email = :email, 
204
					status = :status,
205
					onwikiname = :onwikiname, 
206
					lastactive = :lastactive,
207
					forcelogout = :forcelogout, 
208
					confirmationdiff = :confirmationdiff,
209
                    updateversion = updateversion + 1
210
				WHERE id = :id AND updateversion = :updateversion;
211
SQL
212
            );
213
214
            $statement->bindValue(':id', $this->id);
215
            $statement->bindValue(':updateversion', $this->updateversion);
216
217
            $statement->bindValue(':username', $this->username);
218
            $statement->bindValue(':email', $this->email);
219
            $statement->bindValue(':status', $this->status);
220
            $statement->bindValue(':onwikiname', $this->onwikiname);
221
            $statement->bindValue(':lastactive', $this->lastactive);
222
            $statement->bindValue(':forcelogout', $this->forcelogout);
223
            $statement->bindValue(':confirmationdiff', $this->confirmationdiff);
224
225
            if (!$statement->execute()) {
226
                throw new Exception($statement->errorInfo());
227
            }
228
229
            if ($statement->rowCount() !== 1) {
230
                throw new OptimisticLockFailedException();
231
            }
232
233
            $this->updateversion++;
234
        }
235
    }
236
237
    #region properties
238
239
    /**
240
     * Gets the tool username
241
     * @return string
242
     */
243
    public function getUsername()
244
    {
245
        return $this->username;
246
    }
247
248
    /**
249
     * Sets the tool username
250
     *
251
     * @param string $username
252
     */
253
    public function setUsername($username)
254
    {
255
        $this->username = $username;
256
257
        // If this isn't a brand new user, then it's a rename, force the logout
258
        if (!$this->isNew()) {
259
            $this->forcelogout = 1;
260
        }
261
    }
262
263
    /**
264
     * Gets the user's email address
265
     * @return string
266
     */
267
    public function getEmail()
268
    {
269
        return $this->email;
270
    }
271
272
    /**
273
     * Sets the user's email address
274
     *
275
     * @param string $email
276
     */
277
    public function setEmail($email)
278
    {
279
        $this->email = $email;
280
    }
281
282
    /**
283
     * Gets the status (User, Admin, Suspended, etc - excludes checkuser) of the user.
284
     * @return string
285
     */
286
    public function getStatus()
287
    {
288
        return $this->status;
289
    }
290
291
    /**
292
     * @param string $status
293
     */
294
    public function setStatus($status)
295
    {
296
        $this->status = $status;
297
    }
298
299
    /**
300
     * Gets the user's on-wiki name
301
     * @return string
302
     */
303
    public function getOnWikiName()
304
    {
305
        return $this->onwikiname;
306
    }
307
308
    /**
309
     * Sets the user's on-wiki name
310
     *
311
     * This can have interesting side-effects with OAuth.
312
     *
313
     * @param string $onWikiName
314
     */
315
    public function setOnWikiName($onWikiName)
316
    {
317
        $this->onwikiname = $onWikiName;
318
    }
319
320
    /**
321
     * Gets the last activity date for the user
322
     *
323
     * @return string
324
     * @todo This should probably return an instance of DateTime
325
     */
326
    public function getLastActive()
327
    {
328
        return $this->lastactive;
329
    }
330
331
    /**
332
     * Gets the user's forced logout status
333
     *
334
     * @return bool
335
     */
336
    public function getForceLogout()
337
    {
338
        return $this->forcelogout == 1;
339
    }
340
341
    /**
342
     * Sets the user's forced logout status
343
     *
344
     * @param bool $forceLogout
345
     */
346
    public function setForceLogout($forceLogout)
347
    {
348
        $this->forcelogout = $forceLogout ? 1 : 0;
349
    }
350
351
    /**
352
     * Gets the user's confirmation diff. Unused if OAuth is in use.
353
     * @return int the diff ID
354
     */
355
    public function getConfirmationDiff()
356
    {
357
        return $this->confirmationdiff;
358
    }
359
360
    /**
361
     * Sets the user's confirmation diff.
362
     *
363
     * @param int $confirmationDiff
364
     */
365
    public function setConfirmationDiff($confirmationDiff)
366
    {
367
        $this->confirmationdiff = $confirmationDiff;
368
    }
369
370
    #endregion
371
372
    #region user access checks
373
374
    public function isActive()
375
    {
376
        return $this->status == self::STATUS_ACTIVE;
377
    }
378
379
    /**
380
     * DO NOT USE FOR TESTING IDENTIFICATION STATUS.
381
     *
382
     * This only returns any overrides in the database for identification status,
383
     * and is thus not suitable on its own to determine if a user is identified.
384
     *
385
     * Most (all?) users should have a null value here; this is only here as an
386
     * emergency override in case things go horribly, horribly wrong. For
387
     * example, when WMF completely change the layout of the ID noticeboard.
388
     */
389
    public function getForceIdentified(): ?bool
390
    {
391
        if ($this->forceidentified === null) {
392
            return null;
393
        }
394
395
        return $this->forceidentified === 1;
396
    }
397
398
    /**
399
     * Tests if the user is suspended
400
     * @return bool
401
     * @category Security-Critical
402
     */
403
    public function isSuspended()
404
    {
405
        return $this->status == self::STATUS_SUSPENDED;
406
    }
407
408
    /**
409
     * Tests if the user is new
410
     * @return bool
411
     * @category Security-Critical
412
     */
413
    public function isNewUser()
414
    {
415
        return $this->status == self::STATUS_NEW;
416
    }
417
418
    /**
419
     * Tests if the user has been declined access to the tool
420
     * @return bool
421
     * @category Security-Critical
422
     */
423
    public function isDeclined()
424
    {
425
        return $this->status == self::STATUS_DECLINED;
426
    }
427
428
    /**
429
     * Tests if the user is the community user
430
     *
431
     * @todo     decide if this means logged out. I think it usually does.
432
     * @return bool
433
     * @category Security-Critical
434
     */
435
    public function isCommunityUser()
436
    {
437
        return false;
438
    }
439
440
    #endregion 
441
442
    /**
443
     * Gets the approval date of the user
444
     * @return DateTime|false
445
     */
446
    public function getApprovalDate()
447
    {
448
        $query = $this->dbObject->prepare(<<<SQL
449
			SELECT timestamp 
450
			FROM log 
451
			WHERE objectid = :userid
452
				AND objecttype = 'User'
453
				AND action = 'Approved' 
454
			ORDER BY id DESC 
455
			LIMIT 1;
456
SQL
457
        );
458
        $query->execute(array(":userid" => $this->id));
459
460
        $data = DateTime::createFromFormat("Y-m-d H:i:s", $query->fetchColumn());
461
        $query->closeCursor();
462
463
        return $data;
464
    }
465
}
466