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
introduced
by
![]() |
|||||
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
![]() |
|||||
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 |