Issues (186)

includes/DataObjects/User.php (2 issues)

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_DEACTIVATED = 'Deactivated';
27
    const STATUS_NEW = 'New';
28
29
    private static CommunityUser $community;
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) {
0 ignored issues
show
The condition $user === false is always false.
Loading history...
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 (!isset(self::$community)) {
101
            self::$community = new CommunityUser();
102
        }
103
104
        return self::$community;
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, null,
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(":confirmationdiff", $this->confirmationdiff);
190
191
            if ($statement->execute()) {
192
                $this->id = (int)$this->dbObject->lastInsertId();
193
            }
194
            else {
195
                throw new Exception($statement->errorInfo());
0 ignored issues
show
$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

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