Failed Conditions
Push — startup ( 8a1b3f )
by Simon
06:20
created

includes/DataObjects/User.php (1 issue)

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\DataObjects;
10
11
use DateTime;
12
use Exception;
13
use Waca\DataObject;
14
use Waca\Exceptions\OptimisticLockFailedException;
15
use Waca\IdentificationVerifier;
16
use Waca\PdoDatabase;
17
use Waca\WebRequest;
18
19
/**
20
 * User data object
21
 */
22
class User extends DataObject
23
{
24
    const STATUS_ACTIVE = 'Active';
25
    const STATUS_SUSPENDED = 'Suspended';
26
    const STATUS_DECLINED = 'Declined';
27
    const STATUS_NEW = 'New';
28
29
    private static ?CommunityUser $community = null;
30
31
    private $username;
32
    private $email;
33
    private $status = self::STATUS_NEW;
34
    private $onwikiname;
35
    private $lastactive = "0000-00-00 00:00:00";
36
    private $forcelogout = 0;
37
    private $forceidentified = null;
38
    private $confirmationdiff = 0;
39
    /** @var User Cache variable of the current user - it's never going to change in the middle of a request. */
40
    private static $currentUser;
41
    #region Object load methods
42
43
    /**
44
     * Gets the currently logged in user
45
     *
46
     * @param PdoDatabase $database
47
     *
48
     * @return User|CommunityUser
49
     */
50
    public static function getCurrent(PdoDatabase $database)
51
    {
52
        if (self::$currentUser === null) {
53
            $sessionId = WebRequest::getSessionUserId();
54
55
            if ($sessionId !== null) {
56
                /** @var User $user */
57
                $user = self::getById($sessionId, $database);
58
59
                if ($user === false) {
60
                    self::$currentUser = new CommunityUser();
61
                }
62
                else {
63
                    self::$currentUser = $user;
64
                }
65
            }
66
            else {
67
                $anonymousCoward = new CommunityUser();
68
69
                self::$currentUser = $anonymousCoward;
70
            }
71
        }
72
73
        return self::$currentUser;
74
    }
75
76
    /**
77
     * Gets a user by their user ID
78
     *
79
     * Pass -1 to get the community user.
80
     *
81
     * @param int|null    $id
82
     * @param PdoDatabase $database
83
     *
84
     * @return User|false
85
     */
86
    public static function getById($id, PdoDatabase $database)
87
    {
88
        if ($id === null || $id == -1) {
89
            return new CommunityUser();
90
        }
91
92
        /** @var User|false $user */
93
        $user = parent::getById($id, $database);
94
95
        return $user;
96
    }
97
98
    public static function getCommunity(): CommunityUser
99
    {
100
        if (self::$community === null) {
101
            self::$community = new CommunityUser();
102
        }
103
104
        return self::$community;
0 ignored issues
show
Bug Best Practice introduced by
The expression return self::community could return the type null which is incompatible with the type-hinted return Waca\DataObjects\CommunityUser. Consider adding an additional type-check to rule them out.
Loading history...
105
    }
106
107
    /**
108
     * Gets a user by their username
109
     *
110
     * @param  string      $username
111
     * @param  PdoDatabase $database
112
     *
113
     * @return CommunityUser|User|false
114
     */
115
    public static function getByUsername($username, PdoDatabase $database)
116
    {
117
        if ($username === self::getCommunity()->getUsername()) {
118
            return new CommunityUser();
119
        }
120
121
        $statement = $database->prepare("SELECT * FROM user WHERE username = :id LIMIT 1;");
122
        $statement->bindValue(":id", $username);
123
124
        $statement->execute();
125
126
        $resultObject = $statement->fetchObject(get_called_class());
127
128
        if ($resultObject != false) {
129
            $resultObject->setDatabase($database);
130
        }
131
132
        return $resultObject;
133
    }
134
135
    /**
136
     * Gets a user by their on-wiki username.
137
     *
138
     * @param string      $username
139
     * @param PdoDatabase $database
140
     *
141
     * @return User|false
142
     */
143
    public static function getByOnWikiUsername($username, PdoDatabase $database)
144
    {
145
        $statement = $database->prepare("SELECT * FROM user WHERE onwikiname = :id LIMIT 1;");
146
        $statement->bindValue(":id", $username);
147
        $statement->execute();
148
149
        $resultObject = $statement->fetchObject(get_called_class());
150
151
        if ($resultObject != false) {
152
            $resultObject->setDatabase($database);
153
154
            return $resultObject;
155
        }
156
157
        return false;
158
    }
159
160
    #endregion
161
162
    /**
163
     * Saves the current object
164
     *
165
     * @throws Exception
166
     */
167
    public function save()
168
    {
169
        if ($this->isNew()) {
170
            // insert
171
            $statement = $this->dbObject->prepare(<<<SQL
172
				INSERT INTO `user` ( 
173
					username, email, status, onwikiname, 
174
					lastactive, forcelogout, forceidentified,
175
					confirmationdiff
176
				) VALUES (
177
					:username, :email, :status, :onwikiname,
178
					:lastactive, :forcelogout, :forceidentified,
179
					:confirmationdiff
180
				);
181
SQL
182
            );
183
            $statement->bindValue(":username", $this->username);
184
            $statement->bindValue(":email", $this->email);
185
            $statement->bindValue(":status", $this->status);
186
            $statement->bindValue(":onwikiname", $this->onwikiname);
187
            $statement->bindValue(":lastactive", $this->lastactive);
188
            $statement->bindValue(":forcelogout", $this->forcelogout);
189
            $statement->bindValue(":forceidentified", $this->forceidentified);
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());
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
					forceidentified = :forceidentified,
209
					confirmationdiff = :confirmationdiff,
210
                    updateversion = updateversion + 1
211
				WHERE id = :id AND updateversion = :updateversion;
212
SQL
213
            );
214
            $statement->bindValue(":forceidentified", $this->forceidentified);
215
216
            $statement->bindValue(':id', $this->id);
217
            $statement->bindValue(':updateversion', $this->updateversion);
218
219
            $statement->bindValue(':username', $this->username);
220
            $statement->bindValue(':email', $this->email);
221
            $statement->bindValue(':status', $this->status);
222
            $statement->bindValue(':onwikiname', $this->onwikiname);
223
            $statement->bindValue(':lastactive', $this->lastactive);
224
            $statement->bindValue(':forcelogout', $this->forcelogout);
225
            $statement->bindValue(':forceidentified', $this->forceidentified);
226
            $statement->bindValue(':confirmationdiff', $this->confirmationdiff);
227
228
            if (!$statement->execute()) {
229
                throw new Exception($statement->errorInfo());
230
            }
231
232
            if ($statement->rowCount() !== 1) {
233
                throw new OptimisticLockFailedException();
234
            }
235
236
            $this->updateversion++;
237
        }
238
    }
239
240
    #region properties
241
242
    /**
243
     * Gets the tool username
244
     * @return string
245
     */
246
    public function getUsername()
247
    {
248
        return $this->username;
249
    }
250
251
    /**
252
     * Sets the tool username
253
     *
254
     * @param string $username
255
     */
256
    public function setUsername($username)
257
    {
258
        $this->username = $username;
259
260
        // If this isn't a brand new user, then it's a rename, force the logout
261
        if (!$this->isNew()) {
262
            $this->forcelogout = 1;
263
        }
264
    }
265
266
    /**
267
     * Gets the user's email address
268
     * @return string
269
     */
270
    public function getEmail()
271
    {
272
        return $this->email;
273
    }
274
275
    /**
276
     * Sets the user's email address
277
     *
278
     * @param string $email
279
     */
280
    public function setEmail($email)
281
    {
282
        $this->email = $email;
283
    }
284
285
    /**
286
     * Gets the status (User, Admin, Suspended, etc - excludes checkuser) of the user.
287
     * @return string
288
     */
289
    public function getStatus()
290
    {
291
        return $this->status;
292
    }
293
294
    /**
295
     * @param string $status
296
     */
297
    public function setStatus($status)
298
    {
299
        $this->status = $status;
300
    }
301
302
    /**
303
     * Gets the user's on-wiki name
304
     * @return string
305
     */
306
    public function getOnWikiName()
307
    {
308
        return $this->onwikiname;
309
    }
310
311
    /**
312
     * Sets the user's on-wiki name
313
     *
314
     * This can have interesting side-effects with OAuth.
315
     *
316
     * @param string $onWikiName
317
     */
318
    public function setOnWikiName($onWikiName)
319
    {
320
        $this->onwikiname = $onWikiName;
321
    }
322
323
    /**
324
     * Gets the last activity date for the user
325
     *
326
     * @return string
327
     * @todo This should probably return an instance of DateTime
328
     */
329
    public function getLastActive()
330
    {
331
        return $this->lastactive;
332
    }
333
334
    /**
335
     * Gets the user's forced logout status
336
     *
337
     * @return bool
338
     */
339
    public function getForceLogout()
340
    {
341
        return $this->forcelogout == 1;
342
    }
343
344
    /**
345
     * Sets the user's forced logout status
346
     *
347
     * @param bool $forceLogout
348
     */
349
    public function setForceLogout($forceLogout)
350
    {
351
        $this->forcelogout = $forceLogout ? 1 : 0;
352
    }
353
354
    /**
355
     * Gets the user's confirmation diff. Unused if OAuth is in use.
356
     * @return int the diff ID
357
     */
358
    public function getConfirmationDiff()
359
    {
360
        return $this->confirmationdiff;
361
    }
362
363
    /**
364
     * Sets the user's confirmation diff.
365
     *
366
     * @param int $confirmationDiff
367
     */
368
    public function setConfirmationDiff($confirmationDiff)
369
    {
370
        $this->confirmationdiff = $confirmationDiff;
371
    }
372
373
    #endregion
374
375
    #region user access checks
376
377
    public function isActive()
378
    {
379
        return $this->status == self::STATUS_ACTIVE;
380
    }
381
382
    /**
383
     * Tests if the user is identified
384
     *
385
     * @param IdentificationVerifier $iv
386
     *
387
     * @return bool
388
     * @todo     Figure out what on earth is going on with PDO's typecasting here.  Apparently, it returns string("0") for
389
     *       the force-unidentified case, and int(1) for the identified case?!  This is quite ugly, but probably needed
390
     *       to play it safe for now.
391
     * @category Security-Critical
392
     */
393
    public function isIdentified(IdentificationVerifier $iv)
394
    {
395
        if ($this->forceidentified === 0 || $this->forceidentified === "0") {
396
            // User forced to unidentified in the database.
397
            return false;
398
        }
399
        elseif ($this->forceidentified === 1 || $this->forceidentified === "1") {
400
            // User forced to identified in the database.
401
            return true;
402
        }
403
        else {
404
            // User not forced to any particular identified status; consult IdentificationVerifier
405
            return $iv->isUserIdentified($this->getOnWikiName());
406
        }
407
    }
408
409
    /**
410
     * DO NOT USE FOR TESTING IDENTIFICATION STATUS.
411
     *
412
     * @return bool|null
413
     */
414
    public function getForceIdentified()
415
    {
416
        return $this->forceidentified;
417
    }
418
419
    /**
420
     * Tests if the user is suspended
421
     * @return bool
422
     * @category Security-Critical
423
     */
424
    public function isSuspended()
425
    {
426
        return $this->status == self::STATUS_SUSPENDED;
427
    }
428
429
    /**
430
     * Tests if the user is new
431
     * @return bool
432
     * @category Security-Critical
433
     */
434
    public function isNewUser()
435
    {
436
        return $this->status == self::STATUS_NEW;
437
    }
438
439
    /**
440
     * Tests if the user has been declined access to the tool
441
     * @return bool
442
     * @category Security-Critical
443
     */
444
    public function isDeclined()
445
    {
446
        return $this->status == self::STATUS_DECLINED;
447
    }
448
449
    /**
450
     * Tests if the user is the community user
451
     *
452
     * @todo     decide if this means logged out. I think it usually does.
453
     * @return bool
454
     * @category Security-Critical
455
     */
456
    public function isCommunityUser()
457
    {
458
        return false;
459
    }
460
461
    #endregion 
462
463
    /**
464
     * Gets the approval date of the user
465
     * @return DateTime|false
466
     */
467
    public function getApprovalDate()
468
    {
469
        $query = $this->dbObject->prepare(<<<SQL
470
			SELECT timestamp 
471
			FROM log 
472
			WHERE objectid = :userid
473
				AND objecttype = 'User'
474
				AND action = 'Approved' 
475
			ORDER BY id DESC 
476
			LIMIT 1;
477
SQL
478
        );
479
        $query->execute(array(":userid" => $this->id));
480
481
        $data = DateTime::createFromFormat("Y-m-d H:i:s", $query->fetchColumn());
482
        $query->closeCursor();
483
484
        return $data;
485
    }
486
}
487