User::getUsername()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 0
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: RocketCMS
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
	protected static $getIDByUsernameFromDB_cache = array();
48
49
	public function __construct() {
50
		//
51
	}
52
53
	public function load (int $userID = -1) {
54
		//check, if user is logged in
55
		if ($userID === -1) {
56
			if (isset($_SESSION['logged-in']) && $_SESSION['logged-in'] === true) {
57
				if (!isset($_SESSION['userID']) || empty($_SESSION['userID'])) {
58
					throw new IllegalStateException("userID is not set in session.");
59
				}
60
61
				if (!isset($_SESSION['username']) || empty($_SESSION['username'])) {
62
					throw new IllegalStateException("username is not set in session.");
63
				}
64
65
				$this->userID = (int) $_SESSION['userID'];
66
				$this->username = $_SESSION['username'];
67
				$this->isLoggedIn = true;
68
69
				//TODO: update online state in database
70
			} else {
71
				$this->setGuest();
72
			}
73
		} else if ($userID == 0) {
74
			throw new IllegalArgumentException("userID cannot be 0, userID has to be > 0.");
75
		} else {
76
			$this->userID = (int) $userID;
77
		}
78
79
		Events::throwEvent("before_load_user", array(
80
			'userID' => &$this->userID,
81
			'isLoggedIn' => &$this->isLoggedIn,
82
			'user' => &$this
83
		));
84
85
		//try to load from cache
86
		if (Cache::contains("user", "user-" . $this->userID)) {
87
			$this->row = Cache::get("user", "user-" . $this->userID);
88
		} else {
89
			$row = false;
90
91
			//check, if guest user, because guest user doesnt exists in database
92
			if ($this->userID !== -1) {
93
				$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
94
					'userID' => array(
95
						'type' => PDO::PARAM_INT,
96
						'value' => $this->userID
97
					)
98
				));
99
			}
100
101
			if (!$row) {
102
				$logout_user = true;
103
104
				//user not found, throw an event, so plugins can handle this (optional)
105
				Events::throwEvent("user_not_found", array(
106
					'userID' => &$this->userID,
107
					'username' => &$this->username,
108
					'isLoggedIn' => &$this->isLoggedIn,
109
					'row' => &$row,
110
					'logout_user' => &$logout_user,
111
					'user' => &$this
112
				));
113
114
				if ($logout_user && $this->userID == -1) {
115
					//logout user
116
					$this->logout();
117
				}
118
			} else {
119
				//remove password hash from row
120
				unset($row['password']);
121
122
				Events::throwEvent("before_cache_user", array(
123
					'userID' => &$this->userID,
124
					'username' => &$this->username,
125
					'isLoggedIn' => &$this->isLoggedIn,
126
					'row' => &$row,
127
					'user' => &$this
128
				));
129
130
				//cache entry
131
				Cache::put("user", "user-" . $this->userID, $row);
132
133
				$this->row = $row;
134
			}
135
		}
136
137
		if ($this->row !== null) {
138
			$this->userID = (int) $this->row['userID'];
139
			$this->username = $this->row['username'];
140
		}
141
142
		Events::throwEvent("after_load_user", array(
143
			'userID' => &$this->userID,
144
			'username' => &$this->username,
145
			'isLoggedIn' => &$this->isLoggedIn,
146
			'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...
147
			'user' => &$this
148
		));
149
150
		//TODO: update online state and IP
151
		if ($userID === -1 && $this->isLoggedIn()) {
152
			$this->setOnline();
153
		}
154
	}
155
156
	public function loginByUsername (string $username, string $password) : array {
157
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `username` = :username AND `activated` = '1'; ", array(
158
			'username' => &$username
159
		));
160
161
		if (!$row) {
162
			//get default authentificator
163
			$authentificator = self::getDefaultAuthentificator();
164
165
			$userID = $authentificator->checkPasswordAndImport($username, $password);
166
167
			if ($userID == -1) {
168
				//user not found
169
			} else {
170
				//user was imported now, get user row
171
				$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
172
					'userID' => &$userID
173
				));
174
			}
175
		}
176
177
		return $this->loginRow($row, $password);
178
	}
179
180
	public function loginByMail (string $mail, string $password) : array {
181
		//check, if mail is valide
182
		$validator = new Validator_Mail();
183
184
		if (!$validator->isValide($mail)) {
185
			return array(
186
				'success' => false,
187
				'error' => "mail_not_valide"
188
			);
189
		}
190
191
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `mail` = :mail AND `activated` = '1'; ", array(
192
			'mail' => &$mail
193
		));
194
195
		return $this->loginRow($row, $password);
196
	}
197
198
	public function loginByID (int $userID) : array {
199
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
200
			'userID' => &$userID
201
		));
202
203
		$res = array();
204
205
		if ($row !== false) {
206
			//set online state
207
			$this->setOnline();
208
209
			//set logged in
210
			$this->setLoggedIn($row['userID'], $row['username'], $row);
211
212
			//login successful
213
			$res['success'] = true;
214
			$res['error'] = "none";
215
			return $res;
216
		} else {
217
			//user doesnt exists
218
			$res['success'] = false;
219
			$res['error'] = "user_not_exists";
220
			return $res;
221
		}
222
	}
223
224
	/**
225
	 * check password of current user
226
	 *
227
	 * @param $password string password
228
	 *
229
	 * @throws IllegalStateException if user wasnt loaded before
230
	 *
231
	 * @return true, if password is correct
232
	 */
233
	public function checkPassword (string $password) : bool {
234
		if ($this->row == null || empty($this->row)) {
235
			throw new IllegalStateException("user wasnt loaded.");
236
		}
237
238
		//because password is not cached, we have to load it directly from database
239
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
240
			'userID' => $this->getID()
241
		));
242
243
		//get salt
244
		$salt = $row['salt'];
245
246
		//add salt to password
247
		$password .= $salt;
248
249
		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...
250
	}
251
252
	public function setPassword (string $password) {
253
		if ($this->row == null || empty($this->row)) {
254
			throw new IllegalStateException("user wasnt loaded.");
255
		}
256
257
		//validate password
258
		$password = Validator_Password::get($password);
259
260
		//create new salt
261
		$salt = md5(PHPUtils::randomString(50));
262
263
		//generate password hash
264
		$hashed_password = self::hashPassword($password, $salt);
265
266
		//update database
267
		Database::getInstance()->execute("UPDATE `{praefix}user` SET `password` = :password, `salt` = :salt WHERE `userID` = :userID; ", array(
268
			'password' => $hashed_password,
269
			'salt' => $salt,
270
			'userID' => $this->getID()
271
		));
272
273
		//clear cache
274
		Cache::clear("user", "user-" . $this->getID());
275
	}
276
277
	protected function loginRow ($row, string $password) : array {
278
		$res = array();
279
280
		if (!$row) {
281
			//user doesnt exists
282
			$res['success'] = false;
283
			$res['error'] = "user_not_exists";
284
285
			return $res;
286
		}
287
288
		if ($row['userID'] <= 0) {
289
			//user doesnt exists
290
			$res['success'] = false;
291
			$res['error'] = "not_permitted_user";
292
293
			return $res;
294
		}
295
296
		//get authentificator
297
		$authentificator = self::getAuthentificatorByID($row['userID']);
298
299
		$auth_res = $authentificator->checkPasswordAndImport($row['username'], $password);
300
301
		//check password
302
		if ($auth_res !== -1) {
303
			//password is correct
304
305
			//set online state
306
			$this->setOnline();
307
308
			//set logged in
309
			$this->setLoggedIn($row['userID'], $row['username'], $row);
310
311
			//login successful
312
			$res['success'] = true;
313
			$res['error'] = "none";
314
			return $res;
315
		} else {
316
			//wrong password
317
318
			//user doesnt exists
319
			$res['success'] = false;
320
			$res['error'] = "wrong_password";
321
322
			return $res;
323
		}
324
	}
325
326
	protected function setLoggedIn (int $userID, string $username, array $row) {
327
		$_SESSION['logged-in'] = true;
328
		$_SESSION['userID'] = (int) $userID;
329
		$_SESSION['username'] = $username;
330
331
		//remove password hash from row (so password isnt cached)
332
		unset($row['password']);
333
334
		$this->userID = $userID;
335
		$this->username = $username;
336
		$this->row = $row;
337
	}
338
339
	public function logout () {
340
		//check, if session was started
341
		PHPUtils::checkSessionStarted();
342
343
		unset($_SESSION['userID']);
344
		unset($_SESSION['username']);
345
346
		$_SESSION['logged-in'] = false;
347
348
		$this->setGuest();
349
	}
350
351
	protected function setGuest () {
352
		$this->userID = (int) Settings::get("guest_userid", "-1");
353
		$this->username = Settings::get("guest_username", "Guest");
354
		$this->isLoggedIn = false;
355
	}
356
357
	protected static function hashPassword ($password, $salt) {
358
		//http://php.net/manual/de/function.password-hash.php
359
360
		//add salt to password
361
		$password .= $salt;
362
363
		$options = array(
364
			'cost' => (int) Settings::get("password_hash_cost", "10")
365
		);
366
		$algo = PASSWORD_DEFAULT;
367
368
		Events::throwEvent("hashing_password", array(
369
			'options' => &$options,
370
			'algo' => &$algo
371
		));
372
373
		return password_hash($password, $algo, $options);
374
	}
375
376
	/**
377
	 * get user ID of user
378
	 *
379
	 * @return integer userID
380
	 */
381
	public function getID () : int {
382
		return $this->userID;
383
	}
384
385
	/**
386
	 * get username of user
387
	 *
388
	 * @return string username
389
	 */
390
	public function getUsername () : string {
391
		return $this->username;
392
	}
393
394
	public function getMail () : string {
395
		return $this->row['mail'];
396
	}
397
398
	public function isLoggedIn () : bool {
399
		return $this->isLoggedIn;
400
	}
401
402
	public function getRow () : array {
403
		return $this->row;
404
	}
405
406
	public function hasTitle () : bool {
407
		return $this->row['specific_title'] !== "none";
408
	}
409
410
	public function getTitle () : string {
411
		if (!$this->hasTitle()) {
412
			return Settings::get("user_default_title", "User");
413
		}
414
415
		return $this->row['specific_title'];
416
	}
417
418
	public function setOnline (bool $updateIP = true) {
419
		//get client ip
420
		$ip = PHPUtils::getClientIP();
421
422
		if ($updateIP) {
423
			Database::getInstance()->execute("UPDATE `{praefix}user` SET `online` = '1', `last_online` = CURRENT_TIMESTAMP, `ip` = :ip WHERE `userid` = :userid; ", array(
424
				'userid' => array(
425
					'type' => PDO::PARAM_INT,
426
					'value' => (int) $this->userID
427
				),
428
				'ip' => $ip
429
			));
430
		} else {
431
			Database::getInstance()->execute("UPDATE `{praefix}user` SET `online` = '1', `last_online` = CURRENT_TIMESTAMP, WHERE `userid` = :userid; ", array(
432
				'userid' => array(
433
					'type' => PDO::PARAM_INT,
434
					'value' => (int) $this->userID
435
				)
436
			));
437
		}
438
	}
439
440
	public function updateOnlineList () {
441
		$interval_minutes = (int) Settings::get("online_interval", "5");
442
443
		Database::getInstance()->execute("UPDATE `{praefix}user` SET `online` = '0' WHERE DATE_SUB(NOW(), INTERVAL " . $interval_minutes . " MINUTE) > `last_online`; ");
444
	}
445
446
	/**
447
	 * creates user if userID is absent
448
	 *
449
	 * Only use this method for installation & upgrade!
450
	 */
451
	public static function createIfIdAbsent (int $userID, string $username, string $password, string $mail, int $main_group = 2, string $specific_title = "none", int $activated = 1) {
452
		if (self::existsUserID($userID)) {
453
			//dont create user, if user already exists
454
			return;
455
		}
456
457
		//create salt
458
		$salt = md5(PHPUtils::randomString(50));
459
460
		//generate password hash
461
		$hashed_password = self::hashPassword($password, $salt);
462
463
		Database::getInstance()->execute("INSERT INTO `{praefix}user` (
464
			`userID`, `username`, `password`, `salt`, `mail`, `ip`, `main_group`, `specific_title`, `online`, `last_online`, `registered`, `activated`
465
		) VALUES (
466
			:userID, :username, :password, :salt, :mail, '0.0.0.0', :main_group, :title, '0', '0000-00-00 00:00:00', CURRENT_TIMESTAMP , :activated
467
		)", array(
468
			'userID' => $userID,
469
			'username' => $username,
470
			'password' => $hashed_password,
471
			'salt' => $salt,
472
			'mail' => $mail,
473
			'main_group' => $main_group,
474
			'title' => $specific_title,
475
			'activated' => $activated
476
		));
477
	}
478
479
	public static function create (string $username, string $password, string $mail, string $ip, int $main_group = 2, string $specific_title = "none", int $activated = 1, string $authentificator = "LocalAuthentificator", string $owner = "system") {
480
		if (self::existsUsername($username)) {
481
			//dont create user, if username already exists
482
			return false;
483
		}
484
485
		if (self::existsMail($mail)) {
486
			//dont create user, if mail already exists
487
			return false;
488
		}
489
490
		if (empty($specific_title)) {
491
			$specific_title = "none";
492
		}
493
494
		//create salt
495
		$salt = md5(PHPUtils::randomString(50));
496
497
		//generate password hash
498
		$hashed_password = self::hashPassword($password, $salt);
499
500
		//create user in database
501
		Database::getInstance()->execute("INSERT INTO `{praefix}user` (
502
			`userID`, `username`, `password`, `salt`, `mail`, `ip`, `main_group`, `specific_title`, `online`, `last_online`, `authentificator`, `owner`, `registered`, `activated`
503
		) VALUES (
504
			NULL, :username, :password, :salt, :mail, :ip, :main_group, :title, '0', '0000-00-00 00:00:00', :authentificator, :owner, CURRENT_TIMESTAMP , :activated
505
		)", array(
506
			'username' => $username,
507
			'password' => $hashed_password,
508
			'salt' => $salt,
509
			'mail' => $mail,
510
			'ip' => $ip,
511
			'main_group' => $main_group,
512
			'title' => $specific_title,
513
			'authentificator' => $authentificator,
514
			'owner' => $owner,
515
			'activated' => $activated
516
		));
517
518
		//get userID
519
		$userID = self::getIDByUsernameFromDB($username);
520
521
		if ($userID == Settings::get("guest_userid", -1)) {
522
			//something went wrong
523
			return false;
524
		}
525
526
		//add user to group "registered users"
527
		Groups::addGroupToUser(2, $userID, false);
528
529
		Events::throwEvent("add_user", array(
530
			'userID' => $userID,
531
			'username' => &$username,
532
			'mail' => $mail,
533
			'main_group' => $main_group
534
		));
535
536
		return array(
537
			'success' => true,
538
			'userID' => $userID,
539
			'username' => $username,
540
			'mail' => $mail
541
		);
542
	}
543
544
	public static function deleteUserID (int $userID) {
545
		Database::getInstance()->execute("DELETE FROM `{praefix}user` WHERE `userID` = :userID; ", array(
546
			'userID' => array(
547
				'type' => PDO::PARAM_INT,
548
				'value' => $userID
549
			)
550
		));
551
552
		//remove user from cache
553
		Cache::clear("user", "user-" . $userID);
554
	}
555
556
	public static function existsUserID (int $userID) : bool {
557
		//search for userID in database
558
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID; ", array(
559
			'userID' => array(
560
				'type' => PDO::PARAM_INT,
561
				'value' => $userID
562
			)
563
		));
564
565
		return $row !== false;
566
	}
567
568
	public static function existsUsername (string $username) : bool {
569
		//search for username in database, ignore case
570
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE UPPER(`username`) LIKE UPPER(:username); ", array('username' => $username));
571
572
		return $row !== false;
573
	}
574
575
	public static function existsMail (string $mail) : bool {
576
		//search for mail in database, ignore case
577
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE UPPER(`mail`) LIKE UPPER(:mail); ", array('mail' => $mail));
578
579
		return $row !== false;
580
	}
581
582
	public static function getIDByUsernameFromDB (string $username) : int {
583
		//use in-memory cache to avoid a large amount of sql queries on setup scripts
584
		if (isset(self::$getIDByUsernameFromDB_cache[$username])) {
585
			return self::$getIDByUsernameFromDB_cache[$username];
586
		}
587
588
		//search for username in database, ignore case
589
		$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE UPPER(`username`) LIKE UPPER(:username); ", array('username' => $username));
590
591
		if ($row === false) {
592
			//return guest userID
593
			return Settings::get("guest_userid", -1);
594
		}
595
596
		$userID = $row['userID'];
597
		self::$getIDByUsernameFromDB_cache[$username] = $userID;
598
599
		return $userID;
600
	}
601
602
	public static function &getAuthentificatorByID (int $userID = -1) {
603
		if ($userID == -1) {
604
			//get default authentificator
605
			return self::getDefaultAuthentificator();
606
		} else {
607
			//get authentificator class
608
609
			//check, if user exists
610
			if (!self::existsUserID($userID)) {
611
				throw new IllegalStateException("user with userID '" . $userID . "' doesnt exists.");
612
			}
613
614
			$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `userID` = :userID AND `activated` = '1'; ", array(
615
				'userID' => &$userID
616
			));
617
618
			$class_name = $row['authentificator'];
619
			$obj = new $class_name();
620
621
			return $obj;
622
		}
623
	}
624
625
	public static function &getAuthentificatorByUsername (string $username = "") {
626
		if ($username == null || empty($username)) {
627
			//get default authentificator
628
			return self::getDefaultAuthentificator();
629
		} else {
630
			//get authentificator class
631
632
			//check, if user exists
633
			if (!self::existsUsername($username)) {
634
				throw new IllegalStateException("user with username '" . $username . "' doesnt exists.");
635
			}
636
637
			$row = Database::getInstance()->getRow("SELECT * FROM `{praefix}user` WHERE `username` = :username AND `activated` = '1'; ", array(
638
				'username' => &$username
639
			));
640
641
			$class_name = $row['authentificator'];
642
			$obj = new $class_name();
643
644
			return $obj;
645
		}
646
	}
647
648
	public static function &getDefaultAuthentificator () : IAuthentificator {
649
		if (self::$default_authentificator == null) {
650
			$class_name = Settings::get("default_authentificator", "LocalAuthentificator");
651
			$obj = new $class_name();
652
653
			self::$default_authentificator = $obj;
654
		}
655
656
		return self::$default_authentificator;
657
	}
658
659
	/**
660
	 * get instance of current (logged in / guest) user
661
	 */
662
	public static function &current () : User {
663
		if (self::$instance == null) {
664
			self::$instance = new User();
665
			self::$instance->load();
666
		}
667
668
		return self::$instance;
669
	}
670
671
}
672
673
?>
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...
674