Passed
Push — master ( 0a76e0...9e5734 )
by Justin
04:03
created

User::getMail()   A

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