Passed
Push — master ( 89e660...cac47d )
by Justin
03:54
created

User::setLoggedIn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 7
nc 1
nop 3
1
<?php
2
3
/**
4
 * Copyright (c) 2018 Justin Kuenzel (jukusoft.com)
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 *     http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
20
/**
21
 * Project: JuKuCMS
22
 * License: Apache 2.0 license
23
 * User: Justin
24
 * Date: 15.03.2018
25
 * Time: 15:46
26
 */
27
28
class User {
29
30
	//instance of current (logged-in / guest) user
31
	protected static $instance = null;
32
33
	//current userID
34
	protected $userID = -1;
35
36
	//current username
37
	protected $username = "Guest";
38
39
	//flag, if user is logged in
40
	protected $isLoggedIn = false;
41
42
	//current database row
43
	protected $row = null;
44
45
	protected static $default_authentificator = null;
46
47
	public function __construct() {
48
		//
49
	}
50
51
	public function load (int $userID = -1) {
52
		//check, if user is logged in
53
		if ($userID === -1) {
54
			if (isset($_SESSION['logged-in']) && $_SESSION['logged-in'] === true) {
55
				if (!isset($_SESSION['userID']) || empty($_SESSION['userID'])) {
56
					throw new IllegalStateException("userID is not set in session.");
57
				}
58
59
				if (!isset($_SESSION['username']) || empty($_SESSION['username'])) {
60
					throw new IllegalStateException("username is not set in session.");
61
				}
62
63
				$this->userID = (int) $_SESSION['userID'];
64
				$this->username = $_SESSION['username'];
65
				$this->isLoggedIn = true;
66
67
				//TODO: update online state in database
68
			} else {
69
				$this->setGuest();
70
			}
71
		} else {
72
			$this->userID = (int) $userID;
73
		}
74
75
		Events::throwEvent("before_load_user", array(
76
			'userID' => &$this->userID,
77
			'isLoggedIn' => &$this->isLoggedIn,
78
			'user' => &$this
79
		));
80
81
		//try to load from cache
82
		if (Cache::contains("user", "user-" . $this->userID)) {
83
			$this->row = Cache::get("user", "user-" . $this->userID);
84
		} else {
85
			$row = false;
86
87
			//check, if guest user, because guest user doesnt exists in database
88
			if ($this->userID !== -1) {
89
				$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
90
					'userID' => array(
91
						'type' => PDO::PARAM_INT,
92
						'value' => $this->userID
93
					)
94
				));
95
			}
96
97
			if (!$row) {
98
				$logout_user = true;
99
100
				//user not found, throw an event, so plugins can handle this (optional)
101
				Events::throwEvent("user_not_found", array(
102
					'userID' => &$this->userID,
103
					'username' => &$this->username,
104
					'isLoggedIn' => &$this->isLoggedIn,
105
					'row' => &$row,
106
					'logout_user' => &$logout_user,
107
					'user' => &$this
108
				));
109
110
				if ($logout_user) {
0 ignored issues
show
introduced by
The condition $logout_user is always true.
Loading history...
111
					//logout user
112
					$this->logout();
113
				}
114
			} else {
115
				//remove password hash from row
116
				unset($row['password']);
117
118
				Events::throwEvent("before_cache_user", array(
119
					'userID' => &$this->userID,
120
					'username' => &$this->username,
121
					'isLoggedIn' => &$this->isLoggedIn,
122
					'row' => &$row,
123
					'user' => &$this
124
				));
125
126
				//cache entry
127
				Cache::put("user", "user-" . $this->userID, $row);
128
129
				$this->row = $row;
130
			}
131
		}
132
133
		if ($this->row !== null) {
134
			$this->userID = (int) $this->row['userID'];
135
			$this->username = $this->row['username'];
136
		}
137
138
		Events::throwEvent("after_load_user", array(
139
			'userID' => &$this->userID,
140
			'username' => &$this->username,
141
			'isLoggedIn' => &$this->isLoggedIn,
142
			'row' => &$row,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $row does not seem to be defined for all execution paths leading up to this point.
Loading history...
143
			'user' => &$this
144
		));
145
146
		//TODO: update online state and IP
147
		if ($userID === -1 && $this->isLoggedIn()) {
148
			$this->setOnline();
149
		}
150
	}
151
152
	public function loginByUsername (string $username, string $password) : array {
153
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `username` = :username AND `activated` = '1'; ", array(
154
			'username' => &$username
155
		));
156
157
		return $this->loginRow($row, $password);
158
	}
159
160
	public function loginByMail (string $mail, string $password) : array {
161
		//check, if mail is valide
162
		$validator = new Validator_Mail();
163
164
		if (!$validator->isValide($mail)) {
165
			return array(
166
				'success' => false,
167
				'error' => "mail_not_valide"
168
			);
169
		}
170
171
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `mail` = :mail AND `activated` = '1'; ", array(
172
			'mail' => &$mail
173
		));
174
175
		return $this->loginRow($row, $password);
176
	}
177
178
	public function loginByID (int $userID) : array {
179
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
180
			'userID' => &$userID
181
		));
182
183
		$res = array();
184
185
		if ($row !== false) {
186
			//set online state
187
			$this->setOnline();
188
189
			//set logged in
190
			$this->setLoggedIn($row['userID'], $row['username'], $row);
191
192
			//login successful
193
			$res['success'] = true;
194
			$res['error'] = "none";
195
			return $res;
196
		} else {
197
			//user doesnt exists
198
			$res['success'] = false;
199
			$res['error'] = "user_not_exists";
200
			return $res;
201
		}
202
	}
203
204
	/**
205
	 * check password of current user
206
	 *
207
	 * @param $password string password
208
	 *
209
	 * @throws IllegalStateException if user wasnt loaded before
210
	 *
211
	 * @return true, if password is correct
212
	 */
213
	public function checkPassword (string $password) : bool {
214
		if ($this->row == null || empty($this->row)) {
215
			throw new IllegalStateException("user wasnt loaded.");
216
		}
217
218
		//because password is not cached, we have to load it directly from database
219
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
220
			'userID' => $this->getID()
221
		));
222
223
		//get salt
224
		$salt = $row['salt'];
225
226
		//add salt to password
227
		$password .= $salt;
228
229
		return password_verify($password, $row['password']);
0 ignored issues
show
Bug Best Practice introduced by
The expression return password_verify($...word, $row['password']) returns the type boolean which is incompatible with the documented return type true.
Loading history...
230
	}
231
232
	public function setPassword (string $password) {
233
		if ($this->row == null || empty($this->row)) {
234
			throw new IllegalStateException("user wasnt loaded.");
235
		}
236
237
		//validate password
238
		$password = Validator_Password::get($password);
239
240
		//create new salt
241
		$salt = md5(PHPUtils::randomString(50));
242
243
		//generate password hash
244
		$hashed_password = self::hashPassword($password, $salt);
245
246
		//update database
247
		Database::getInstance()->execute("UPDATE `{praefix}user` SET `password` = :password, `salt` = :salt WHERE `userID` = :userID; ", array(
248
			'password' => $hashed_password,
249
			'salt' => $salt,
250
			'userID' => $this->getID()
251
		));
252
253
		//clear cache
254
		Cache::clear("user", "user-" . $this->getID());
255
	}
256
257
	protected function loginRow ($row, string $password) : array {
258
		if (!$row) {
259
			//user doesnt exists
260
			$res['success'] = false;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$res was never initialized. Although not strictly required by PHP, it is generally a good practice to add $res = array(); before regardless.
Loading history...
261
			$res['error'] = "user_not_exists";
262
263
			return $res;
264
		}
265
266
		//user exists
267
268
		//get salt
269
		$salt = $row['salt'];
270
271
		//add salt to password
272
		$password .= $salt;
273
274
		//verify password
275
		if (password_verify($password, $row['password'])) {
276
			//correct password
277
278
			//check, if a newer password algorithmus is available --> rehash required
279
			if (password_needs_rehash($row['password'], PASSWORD_DEFAULT)) {
280
				//rehash password
281
				$new_hash = self::hashPassword($password, $salt);
282
283
				//update password in database
284
				Database::getInstance()->execute("UPDATE `{praefix}user` SET `password` = :password WHERE `userID` = :userID; ", array(
285
					'password' => $new_hash,
286
					'userID' => array(
287
						'type' => PDO::PARAM_INT,
288
						'value' => $row['userID']
289
					)
290
				));
291
			}
292
293
			//set online state
294
			$this->setOnline();
295
296
			//set logged in
297
			$this->setLoggedIn($row['userID'], $row['username'], $row);
298
299
			//login successful
300
			$res['success'] = true;
301
			$res['error'] = "none";
302
			return $res;
303
		} else {
304
			//wrong password
305
306
			//user doesnt exists
307
			$res['success'] = false;
308
			$res['error'] = "wrong_password";
309
310
			return $res;
311
		}
312
	}
313
314
	protected function setLoggedIn (int $userID, string $username, array $row) {
315
		$_SESSION['logged-in'] = true;
316
		$_SESSION['userID'] = (int) $userID;
317
		$_SESSION['username'] = $username;
318
319
		//remove password hash from row (so password isnt cached)
320
		unset($row['password']);
321
322
		$this->userID = $userID;
323
		$this->username = $username;
324
		$this->row = $row;
325
	}
326
327
	public function logout () {
328
		//check, if session was started
329
		PHPUtils::checkSessionStarted();
330
331
		unset($_SESSION['userID']);
332
		unset($_SESSION['username']);
333
334
		$_SESSION['logged-in'] = false;
335
336
		$this->setGuest();
337
	}
338
339
	protected function setGuest () {
340
		$this->userID = (int) Settings::get("guest_userid", "-1");
341
		$this->username = Settings::get("guest_username", "Guest");
342
		$this->isLoggedIn = false;
343
	}
344
345
	protected static function hashPassword ($password, $salt) {
346
		//http://php.net/manual/de/function.password-hash.php
347
348
		//add salt to password
349
		$password .= $salt;
350
351
		$options = array(
352
			'cost' => (int) Settings::get("password_hash_cost", "10")
353
		);
354
		$algo = PASSWORD_DEFAULT;
355
356
		Events::throwEvent("hashing_password", array(
357
			'options' => &$options,
358
			'algo' => &$algo
359
		));
360
361
		return password_hash($password, $algo, $options);
362
	}
363
364
	/**
365
	 * get user ID of user
366
	 *
367
	 * @return integer userID
368
	 */
369
	public function getID () : int {
370
		return $this->userID;
371
	}
372
373
	/**
374
	 * get username of user
375
	 *
376
	 * @return string username
377
	 */
378
	public function getUsername () : string {
379
		return $this->username;
380
	}
381
382
	public function getMail () : string {
383
		return $this->row['mail'];
384
	}
385
386
	public function isLoggedIn () : bool {
387
		return $this->isLoggedIn;
388
	}
389
390
	public function getRow () : array {
391
		return $this->row;
392
	}
393
394
	public function setOnline (bool $updateIP = true) {
395
		//get client ip
396
		$ip = PHPUtils::getClientIP();
397
398
		if ($updateIP) {
399
			Database::getInstance()->execute("UPDATE `{praefix}user` SET `online` = '1', `last_online` = CURRENT_TIMESTAMP, `ip` = :ip WHERE `userid` = :userid; ", array(
400
				'userid' => array(
401
					'type' => PDO::PARAM_INT,
402
					'value' => (int) $this->userID
403
				),
404
				'ip' => $ip
405
			));
406
		} else {
407
			Database::getInstance()->execute("UPDATE `{praefix}user` SET `online` = '1', `last_online` = CURRENT_TIMESTAMP, WHERE `userid` = :userid; ", array(
408
				'userid' => array(
409
					'type' => PDO::PARAM_INT,
410
					'value' => (int) $this->userID
411
				)
412
			));
413
		}
414
	}
415
416
	public function updateOnlineList () {
417
		$interval_minutes = (int) Settings::get("online_interval", "5");
418
419
		Database::getInstance()->execute("UPDATE `{praefix}user` SET `online` = '0' WHERE DATE_SUB(NOW(), INTERVAL " . $interval_minutes . " MINUTE) > `last_online`; ");
420
	}
421
422
	/**
423
	 * creates user if userID is absent
424
	 *
425
	 * Only use this method for installation & upgrade!
426
	 */
427
	public static function createIfIdAbsent (int $userID, string $username, string $password, string $mail, int $main_group = 2, string $specific_title = "none", int $activated = 1) {
428
		if (self::existsUserID($userID)) {
429
			//dont create user, if user already exists
430
			return;
431
		}
432
433
		//create salt
434
		$salt = md5(PHPUtils::randomString(50));
435
436
		//generate password hash
437
		$hashed_password = self::hashPassword($password, $salt);
438
439
		Database::getInstance()->execute("INSERT INTO `{praefix}user` (
440
			`userID`, `username`, `password`, `salt`, `mail`, `ip`, `main_group`, `specific_title`, `online`, `last_online`, `registered`, `activated`
441
		) VALUES (
442
			:userID, :username, :password, :salt, :mail, '0.0.0.0', :main_group, :title, '0', '0000-00-00 00:00:00', CURRENT_TIMESTAMP , :activated
443
		)", array(
444
			'userID' => $userID,
445
			'username' => $username,
446
			'password' => $hashed_password,
447
			'salt' => $salt,
448
			'mail' => $mail,
449
			'main_group' => $main_group,
450
			'title' => $specific_title,
451
			'activated' => $activated
452
		));
453
	}
454
455
	public static function create (string $username, string $password, string $mail, string $ip, int $main_group = 2, string $specific_title = "none", int $activated = 1) {
456
		if (self::existsUsername($username)) {
457
			//dont create user, if username already exists
458
			return false;
459
		}
460
461
		if (self::existsMail($mail)) {
462
			//dont create user, if mail already exists
463
			return false;
464
		}
465
466
		if (empty($specific_title)) {
467
			$specific_title = "none";
468
		}
469
470
		//create salt
471
		$salt = md5(PHPUtils::randomString(50));
472
473
		//generate password hash
474
		$hashed_password = self::hashPassword($password, $salt);
475
476
		//create user in database
477
		Database::getInstance()->execute("INSERT INTO `{praefix}user` (
478
			`userID`, `username`, `password`, `salt`, `mail`, `ip`, `main_group`, `specific_title`, `online`, `last_online`, `registered`, `activated`
479
		) VALUES (
480
			NULL, :username, :password, :salt, :mail, :ip, :main_group, :title, '0', '0000-00-00 00:00:00', CURRENT_TIMESTAMP , :activated
481
		)", array(
482
			'username' => $username,
483
			'password' => $hashed_password,
484
			'salt' => $salt,
485
			'mail' => $mail,
486
			'ip' => $ip,
487
			'main_group' => $main_group,
488
			'title' => $specific_title,
489
			'activated' => $activated
490
		));
491
492
		//get userID
493
		$userID = self::getIDByUsernameFromDB($username);
494
495
		if ($userID == Settings::get("guest_userid", -1)) {
496
			//something went wrong
497
			return false;
498
		}
499
500
		//add user to group "registered users"
501
		Groups::addGroupToUser(2, $userID, false);
502
503
		Events::throwEvent("add_user", array(
504
			'userID' => $userID,
505
			'username' => &$username,
506
			'mail' => $mail,
507
			'main_group' => $main_group
508
		));
509
510
		return array(
511
			'success' => true,
512
			'userID' => $userID,
513
			'username' => $username,
514
			'mail' => $mail
515
		);
516
	}
517
518
	public static function deleteUserID (int $userID) {
519
		Database::getInstance()->execute("DELETE FROM `{praefix}user` WHERE `userID` = :userID; ", array(
520
			'userID' => array(
521
				'type' => PDO::PARAM_INT,
522
				'value' => $userID
523
			)
524
		));
525
526
		//remove user from cache
527
		Cache::clear("user", "user-" . $userID);
528
	}
529
530
	public static function existsUserID (int $userID) : bool {
531
		//search for userID in database
532
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID; ", array(
533
			'userID' => array(
534
				'type' => PDO::PARAM_INT,
535
				'value' => $userID
536
			)
537
		));
538
539
		return $row !== false;
540
	}
541
542
	public static function existsUsername (string $username) : bool {
543
		//search for username in database, ignore case
544
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE UPPER(`username`) LIKE UPPER(:username); ", array('username' => $username));
545
546
		return $row !== false;
547
	}
548
549
	public static function existsMail (string $mail) : bool {
550
		//search for mail in database, ignore case
551
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE UPPER(`mail`) LIKE UPPER(:mail); ", array('mail' => $mail));
552
553
		return $row !== false;
554
	}
555
556
	public static function getIDByUsernameFromDB (string $username) : int {
557
		//search for username in database, ignore case
558
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE UPPER(`username`) LIKE UPPER(:username); ", array('username' => $username));
559
560
		if ($row === false) {
561
			//return guest userID
562
			return Settings::get("guest_userid", -1);
563
		}
564
565
		return $row['userID'];
566
	}
567
568
	public static function &getAuthentificatorByID (int $userID = -1) {
569
		if ($userID == -1) {
570
			//get default authentificator
571
			return self::getDefaultAuthentificator();
572
		} else {
573
			//get authentificator class
574
575
			//check, if user exists
576
			if (!self::existsUserID($userID)) {
577
				throw new IllegalStateException("user with userID '" . $userID . "' doesnt exists.");
578
			}
579
580
			$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
581
				'userID' => &$userID
582
			));
583
584
			$class_name = $row['authentificator'];
585
			return new $class_name();
586
		}
587
	}
588
589
	public static function &getAuthentificatorByUsername (string $username = "") {
590
		if ($username == null || empty($username)) {
591
			//get default authentificator
592
			return self::getDefaultAuthentificator();
593
		} else {
594
			//get authentificator class
595
596
			//check, if user exists
597
			if (!self::existsUsername($username)) {
598
				throw new IllegalStateException("user with username '" . $username . "' doesnt exists.");
599
			}
600
601
			$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `username` = :username AND `activated` = '1'; ", array(
602
				'username' => &$username
603
			));
604
605
			$class_name = $row['authentificator'];
606
			return new $class_name();
607
		}
608
	}
609
610
	public static function &getDefaultAuthentificator () : IAuthentificator {
611
		if (self::$default_authentificator == null) {
612
			$class_name = Settings::get("default_authentificator", "LocalAuthentificator");
613
			$obj = new $class_name();
614
615
			self::$default_authentificator = $obj;
616
		}
617
618
		return self::$default_authentificator;
619
	}
620
621
	/**
622
	 * get instance of current (logged in / guest) user
623
	 */
624
	public static function &current () : User {
625
		if (self::$instance == null) {
626
			self::$instance = new User();
627
			self::$instance->load();
628
		}
629
630
		return self::$instance;
631
	}
632
633
}
634
635
?>
0 ignored issues
show
Best Practice introduced by
It is not recommended to use PHP's closing tag ?> in files other than templates.

Using a closing tag in PHP files that only contain PHP code is not recommended as you might accidentally add whitespace after the closing tag which would then be output by PHP. This can cause severe problems, for example headers cannot be sent anymore.

A simple precaution is to leave off the closing tag as it is not required, and it also has no negative effects whatsoever.

Loading history...
636