Passed
Push — master ( 7c7732...5d308c )
by Stone
03:35
created

UserModel::getAuthorDetails()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace App\Models;
4
5
use App\Modules\Token;
0 ignored issues
show
Bug introduced by
The type App\Modules\Token was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use Core\BlogocException;
7
use Core\Constant;
8
use Core\Container;
9
use Core\Model;
10
11
class UserModel extends Model
12
{
13
14
    private $userTbl;
15
    private $roleTbl;
16
17
    /**
18
     * UserModel constructor.
19
     * @param Container $container
20
     */
21
    public function __construct(Container $container)
22
    {
23
        parent::__construct($container);
24
        $this->userTbl = $this->getTablePrefix("users");
25
        $this->roleTbl = $this->getTablePrefix("roles");
26
    }
27
28
    /**
29
     * the basic select query. We can add restrictions on after in the functions.
30
     * @return string
31
     */
32
    private function baseSqlSelect(): string
33
    {
34
        $sql = "
35
            SELECT idusers, username, avatar, email, surname, name, creation_date, last_update, locked_out, bad_login_time, bad_login_tries, roles_idroles, role_name, role_level, reset_password_hash, reset_password_hash_generation_datetime
36
            FROM $this->userTbl
37
            INNER JOIN $this->roleTbl ON $this->userTbl.roles_idroles = $this->roleTbl.idroles 
38
        ";
39
        return $sql;
40
    }
41
42
    /**
43
     * get the password from the user email. mainly for login purposes
44
     * @param string $email
45
     * @return string
46
     * @throws BlogocException
47
     */
48
    private function getUserPassword(string $email): string
49
    {
50
        if (!$this->isEmailUsed($email)) {
51
            throw new BlogocException("Email not present in Database");
52
        }
53
        $sql = "SELECT password FROM $this->userTbl WHERE email = :email";
54
        $this->query($sql);
55
        $this->bind(':email', $email);
56
        $this->execute();
57
        return $this->stmt->fetchColumn();
58
    }
59
60
    /**
61
     * called when authentication failed
62
     * @param $user
63
     * @throws \Exception
64
     */
65
    private function addToBadLoginTries($user): void
66
    {
67
        $badLoginTries = $user->bad_login_tries + 1;
68
        $sql = "
69
            UPDATE $this->userTbl
70
            SET
71
              bad_login_time = NOW(),
72
              bad_login_tries = :badLoginTries
73
            WHERE idusers = :userId
74
        ";
75
        $this->query($sql);
76
        $this->bind(':badLoginTries', $badLoginTries);
77
        $this->bind(':userId', $user->idusers);
78
        $this->execute();
79
    }
80
81
    /**
82
     * reset the bad login count
83
     * @param $user
84
     * @throws \Exception
85
     */
86
    private function resetBadLogin($user): void
87
    {
88
        $sql = "
89
            UPDATE $this->userTbl
90
            SET
91
              bad_login_tries = 0
92
            WHERE idusers = :userId
93
        ";
94
        $this->query($sql);
95
        $this->bind(':userId', $user->idusers);
96
        $this->execute();
97
    }
98
99
    private function isAccountPasswordBlocked($user)
100
    {
101
        if ($user->bad_login_tries < Constant::NUMBER_OF_BAD_PASSWORD_TRIES) {
102
            //not enough bad tries yet
103
            return false;
104
        }
105
106
        $blockTime = strtotime($user->bad_login_time);
107
        $currentTime = time();
108
        if ($currentTime - $blockTime > Constant::LOCKOUT_MINUTES * 60) {
109
            //we have outlived the timeout, connection authorised
110
            return false;
111
        }
112
        //the account is timed out
113
        return true;
114
    }
115
116
    /**
117
     * set the last update time to now for a user
118
     * @param $user
119
     * @throws \Exception
120
     */
121
    private function setLastUpdateTime($user):void
122
    {
123
        $sql = "
124
            UPDATE $this->userTbl
125
            SET last_update = NOW();
126
            WHERE idusers = :userId
127
        ";
128
        $this->query($sql);
129
        $this->bind(':userId', $user->idusers);
130
        $this->execute();
131
    }
132
133
    /**
134
     * Get all the useful data about a user from his ID
135
     * @param int $userId
136
     * @return mixed
137
     * @throws \Exception
138
     */
139
    public function getUserDetailsById(int $userId)
140
    {
141
        $sql = $this->baseSqlSelect();
142
        $sql .= "
143
            WHERE idusers = :userId
144
        ";
145
        $this->query($sql);
146
        $this->bind(':userId', $userId);
147
        $this->execute();
148
        return $this->fetch();
149
    }
150
151
    /**
152
     * get the user details from a password reset token and Id (for security)
153
     * @param string $token
154
     * @param int $userId
155
     * @return mixed
156
     * @throws \Exception
157
     */
158
    public function getUserDetailsByToken(string $token, int $userId)
159
    {
160
        $hash = $this->generateHash($token);
161
        $sql = $this->baseSqlSelect();
162
        $sql .= "
163
            WHERE reset_password_hash = :token AND idusers = :userId
164
        ";
165
        $this->query($sql);
166
        $this->bind(':token', $hash);
167
        $this->bind(':userId', $userId);
168
        $this->execute();
169
        $user = $this->fetch();
170
        $linkValidTime = strtotime($user->reset_password_hash_generation_datetime);
171
        $currentTime = time();
172
        if ($currentTime - $linkValidTime > Constant::PASSWORD_RESET_DURATION * 60) {
173
            //token is no longer valid
174
            return false;
175
        }
176
        return $user;
177
178
    }
179
180
    /**
181
     * Get all the useful data about a user from his mail
182
     * @param string $email
183
     * @return mixed
184
     * @throws BlogocException
185
     */
186
    public function getUserDetailsByEmail(string $email)
187
    {
188
        //check if email is valid for sanity
189
        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
190
            $email = htmlspecialchars($email);
191
            throw new BlogocException("invalid email " . $email);
192
        }
193
        $sql = $this->baseSqlSelect();
194
        $sql .= "
195
            WHERE email = :email
196
        ";
197
        $this->query($sql);
198
        $this->bind(':email', $email);
199
        $this->execute();
200
        return $this->fetch();
201
    }
202
203
    /**
204
     * check if the email is present in the database
205
     * @param string $email
206
     * @return bool
207
     * @throws \Exception
208
     */
209
    public function isEmailUsed(string $email)
210
    {
211
        return $this->getUserDetailsByEmail($email) !== false;
212
    }
213
214
    /**
215
     * register a new user
216
     * @param \stdClass $userData
217
     * @return int
218
     * @throws \Exception
219
     */
220
    public function registerUser(\stdClass $userData): int
221
    {
222
        $sql = "
223
            INSERT INTO $this->userTbl (username, email, surname, name, creation_date, last_update, roles_idroles, locked_out, bad_login_tries)
224
            VALUES (:username, :email, :surname, :name, NOW(), NOW(), :roles_idroles, 1, 0)
225
        ";
226
        $this->query($sql);
227
        $this->bind(':username', $userData->username);
228
        $this->bind(':email', $userData->email);
229
        $this->bind(':surname', $userData->surname);
230
        $this->bind(':name', $userData->name);
231
        $this->bind(':roles_idroles', 1); //we set to one, should probably get from database and config
232
        $this->execute();
233
234
        return (int)$this->dbh->lastInsertId();
235
    }
236
237
    /**
238
     * update an existing user
239
     * @param \stdClass $user
240
     * @throws \Exception
241
     */
242
    public function updateUser(\stdClass $user)
243
    {
244
        $sql="
245
            UPDATE $this->userTbl
246
            SET
247
              name=:name,
248
              surname=:surname,
249
              username=:username,
250
              avatar=:avatar,
251
              roles_idroles=:userRoleId,
252
              locked_out=:userLockedOut,
253
              last_update = NOW()
254
            WHERE idusers = :userId
255
        ";
256
        $this->query($sql);
257
        $this->bind(':name', $user->userName);
258
        $this->bind(':surname', $user->userSurname);
259
        $this->bind(':username', $user->userUsername);
260
        $this->bind(':avatar', $user->userImage);
261
        $this->bind(':userRoleId', $user->userRoleSelector);
262
        $this->bind(':userLockedOut', $user->userLockedOut);
263
        $this->bind(':userId', $user->userId);
264
        $this->execute();
265
    }
266
267
    /**
268
     * verify the user connection mail/password and login if ok
269
     * @param string $email
270
     * @param string $password
271
     * @return bool|mixed
272
     * @throws BlogocException
273
     */
274
    public function authenticateUser(string $email, string $password): \stdClass
275
    {
276
        $response = new \stdClass();
277
        $response->success = false;
278
        $response->message = "";
279
280
        $user = $this->getUserDetailsByEmail($email);
281
282
        if ($user === false) //no user exists
283
        {
284
            $response->message = "email doesn't exist, register a new account?";
285
            return $response;
286
        }
287
288
        //check if the user has validated his email
289
        if ($user->locked_out) {
290
            $response->message = "the email has not been verified, please check your inbox or click on 'reset your password'";
291
            return $response;
292
        }
293
294
        if ($this->isAccountPasswordBlocked($user)) {
295
            $response->message = "too many bad passwords, account is blocked for " . Constant::LOCKOUT_MINUTES . " minutes";
296
            return $response;
297
        }
298
299
        if (!password_verify($password, $this->getUserPassword($email))) {
300
            $response->message = "password is incorrect";
301
            $this->addToBadLoginTries($user);
302
            return $response;
303
        }
304
305
306
        //all ok, send user back for login
307
        $this->resetBadLogin($user);
308
        $this->setLastUpdateTime($user);
309
        $response->user = $user;
310
        $response->success = true;
311
        return $response;
312
    }
313
314
    /**
315
     * generate a password hash for resetting or defining the password
316
     * @param int $userId
317
     * @return string the generated token
318
     * @throws \Exception
319
     */
320
    public function generatePasswordHash(int $userId): string
321
    {
322
        $user = $this->getUserDetailsById($userId);
323
        if (!$user) {
324
            //user Id doesn't exist, bail out
325
            throw new \Exception("User not found");
326
        }
327
        $token = $this->generateToken();
328
        $hash = $this->generateHash($token);
329
330
        $sql = "
331
            UPDATE $this->userTbl
332
            SET
333
              reset_password_hash = :hash,
334
              reset_password_hash_generation_datetime = NOW()
335
            WHERE idusers = :userId
336
        ";
337
        $this->query($sql);
338
        $this->bind(':hash', $hash);
339
        $this->bind(':userId', $user->idusers);
340
        $this->execute();
341
342
        return $token;
343
    }
344
345
    /**
346
     * Reset the user password
347
     * @param int $userId
348
     * @param string $password
349
     * @throws \Exception
350
     */
351
    public function resetPassword(int $userId, string $password)
352
    {
353
        $hash = password_hash($password, PASSWORD_DEFAULT);
354
        $sql = "
355
            UPDATE $this->userTbl
356
            SET
357
              password = :password,
358
              locked_out = 0,
359
              last_update = NOW()
360
            WHERE idusers = :userId
361
        ";
362
        $this->query($sql);
363
        $this->bind(':password', $hash);
364
        $this->bind(':userId', $userId);
365
        $this->execute();
366
    }
367
368
    /**
369
     * counts all the users
370
     * @return int
371
     * @throws \Exception
372
     */
373
    public function countUsers(): int
374
    {
375
        return $this->count($this->userTbl);
376
    }
377
378
379
    /**
380
     * get the list of all users
381
     * @param int $offset
382
     * @param int $limit
383
     * @return array
384
     * @throws \ReflectionException
385
     */
386
    public function getUserList(int $offset = 0, int $limit = Constant::LIST_PER_PAGE)
387
    {
388
        //return $this->list($offset, $limit, $this->userTbl);
389
        $sql = $this->baseSqlSelect();
390
        $sql .= "
391
            LIMIT :limit OFFSET :offset
392
        ";
393
        $this->query($sql);
394
        $this->bind(":limit", $limit);
395
        $this->bind(":offset", $offset);
396
        $this->execute();
397
        return $this->fetchAll();
398
    }
399
400
    public function activateUser(bool $activation, int $userId)
401
    {
402
        $sql = "
403
          UPDATE $this->userTbl
404
            SET
405
              locked_out = :activation
406
            WHERE idusers = :userId
407
        ";
408
        $this->query($sql);
409
        $this->bind(':activation', $activation);
410
        $this->bind(':userId', $userId);
411
        return $this->execute();
412
    }
413
414
    /**
415
     * delete a user from the dataBase
416
     * @param $userId
417
     * @return bool
418
     * @throws \Exception
419
     */
420
    public function deleteUser(int $userId)
421
    {
422
        $sql = "
423
          DELETE FROM $this->userTbl 
424
          WHERE idusers = :userId
425
        ";
426
427
        $this->query($sql);
428
        $this->bind(':userId', $userId);
429
        return $this->execute();
430
    }
431
432
}