Passed
Push — hypernext ( 403616...d6fe90 )
by Nico
11:05
created

Users::update()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 41
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 0
Metric Value
cc 4
eloc 23
nc 8
nop 1
dl 0
loc 41
ccs 0
cts 16
cp 0
crap 20
rs 9.552
c 0
b 0
f 0
1
<?php
2
/**
3
 * @author Nicolas CARPi <[email protected]>
4
 * @copyright 2012 Nicolas CARPi
5
 * @see https://www.elabftw.net Official website
6
 * @license AGPL-3.0
7
 * @package elabftw
8
 */
9
declare(strict_types=1);
10
11
namespace Elabftw\Models;
12
13
use Elabftw\Elabftw\Db;
14
use Elabftw\Exceptions\ImproperActionException;
15
use Elabftw\Exceptions\ResourceNotFoundException;
16
use Elabftw\Interfaces\ContentParamsInterface;
17
use Elabftw\Services\Check;
18
use Elabftw\Services\Email;
19
use Elabftw\Services\EmailValidator;
20
use Elabftw\Services\Filter;
21
use Elabftw\Services\TeamsHelper;
22
use Elabftw\Services\UsersHelper;
23
use function filter_var;
24
use function hash;
25
use function mb_strlen;
26
use function password_hash;
27
use PDO;
28
use function time;
29
30
/**
31
 * Users
32
 */
33
class Users
34
{
35
    public bool $needValidation = false;
36
37
    public array $userData = array();
38
39
    public int $team = 0;
40
41
    protected Db $Db;
42
43 83
    public function __construct(?int $userid = null, ?int $team = null)
44
    {
45 83
        $this->Db = Db::getConnection();
46 83
        if ($team !== null) {
47 83
            $this->team = $team;
48
        }
49 83
        if ($userid !== null) {
50
            $this->populate($userid);
51
        }
52
    }
53
54
    /**
55
     * Populate userData property
56 83
     */
57
    public function populate(int $userid): void
58 83
    {
59 83
        Check::idOrExplode($userid);
60 83
        $this->userData = $this->getUserData($userid);
61
        $this->userData['team'] = $this->team;
62
    }
63
64
    /**
65
     * Create a new user
66
     */
67
    public function create(
68
        string $email,
69
        array $teams,
70
        string $firstname = '',
71
        string $lastname = '',
72
        string $password = '',
73
        ?int $group = null,
74
        bool $forceValidation = false,
75
        bool $alertAdmin = true,
76
    ): int {
77
        $Config = Config::getConfig();
78
        $Teams = new Teams($this);
79
80
        // make sure that all the teams in which the user will be are created/exist
81
        // this might throw an exception if the team doesn't exist and we can't create it on the fly
82
        $teams = $Teams->getTeamsFromIdOrNameOrOrgidArray($teams);
83
84
        $EmailValidator = new EmailValidator($email, $Config->configArr['email_domain']);
85
        $EmailValidator->validate();
86
87
        if ($password !== '') {
88
            Check::passwordLength($password);
89
        }
90
91
        $firstname = filter_var($firstname, FILTER_SANITIZE_STRING);
92
        $lastname = filter_var($lastname, FILTER_SANITIZE_STRING);
93
94
        // Create password hash
95
        $passwordHash = password_hash($password, PASSWORD_DEFAULT);
96
97
        // Registration date is stored in epoch
98
        $registerDate = time();
99
100
        // get the group for the new user
101
        if ($group === null) {
102
            $teamId = (int) $teams[0]['id'];
103
            $TeamsHelper = new TeamsHelper($teamId);
104
            $group = $TeamsHelper->getGroup();
105
        }
106
107
        // will new user be validated?
108
        $validated = $Config->configArr['admin_validate'] && ($group === 4) ? 0 : 1;
109
        if ($forceValidation) {
110
            $validated = 1;
111
        }
112
113
114
        $sql = 'INSERT INTO users (
115
            `email`,
116
            `password_hash`,
117
            `firstname`,
118
            `lastname`,
119
            `usergroup`,
120
            `register_date`,
121
            `validated`,
122
            `lang`
123
        ) VALUES (
124
            :email,
125
            :password_hash,
126
            :firstname,
127
            :lastname,
128
            :usergroup,
129
            :register_date,
130
            :validated,
131
            :lang);';
132
        $req = $this->Db->prepare($sql);
133
134
        $req->bindParam(':email', $email);
135
        $req->bindParam(':password_hash', $passwordHash);
136
        $req->bindParam(':firstname', $firstname);
137
        $req->bindParam(':lastname', $lastname);
138
        $req->bindParam(':register_date', $registerDate);
139
        $req->bindParam(':validated', $validated, PDO::PARAM_INT);
140
        $req->bindParam(':usergroup', $group, PDO::PARAM_INT);
141
        $req->bindValue(':lang', $Config->configArr['lang']);
142
        $this->Db->execute($req);
143
        $userid = $this->Db->lastInsertId();
144
145
        // now add the user to the team
146
        $Teams->addUserToTeams($userid, array_column($teams, 'id'));
147
        $userInfo = array('email' => $email, 'name' => $firstname . ' ' . $lastname);
148
        $Email = new Email($Config, $this);
149
        // just skip this if we don't have proper normalized teams
150
        if ($alertAdmin && isset($teams[0]['id'])) {
151
            $Email->alertAdmin((int) $teams[0]['id'], $userInfo, !(bool) $validated);
152
        }
153
        if ($validated === 0) {
154
            $Email->alertUserNeedValidation($email);
155
            // set a flag to show correct message to user
156
            $this->needValidation = true;
157
        }
158
        return $userid;
159
    }
160
161
    /**
162
     * Get users matching a search term for consumption in autocomplete
163
     */
164
    public function read(ContentParamsInterface $params): array
165
    {
166
        $usersArr = $this->readFromQuery($params->getContent());
167
        $res = array();
168
        foreach ($usersArr as $user) {
169
            $res[] = $user['userid'] . ' - ' . $user['fullname'];
170
        }
171
        return $res;
172
    }
173
174
    /**
175
     * Search users based on query. It searches in email, firstname, lastname or team name
176
     *
177
     * @param string $query the searched term
178
     * @param bool $teamFilter toggle between sysadmin/admin view
179
     */
180 83
    public function readFromQuery(string $query, bool $teamFilter = false): array
181
    {
182 83
        $teamFilterSql = '';
183
        if ($teamFilter) {
184
            $teamFilterSql = 'AND users2teams.teams_id = :team';
185
        }
186 83
187 83
        // NOTE: previously, the ORDER BY started with the team, but that didn't work
188 83
        // with the DISTINCT, so it was removed.
189
        $sql = "SELECT DISTINCT users.userid,
190
            users.firstname, users.lastname, users.email, users.mfa_secret,
191 83
            users.validated, users.usergroup, users.archived, users.last_login,
192 83
            CONCAT(users.firstname, ' ', users.lastname) AS fullname,
193
            users.cellphone, users.phone, users.website, users.skype
194
            FROM users
195
            CROSS JOIN users2teams ON (users2teams.users_id = users.userid " . $teamFilterSql . ')
196 83
            WHERE (users.email LIKE :query OR users.firstname LIKE :query OR users.lastname LIKE :query)
197
            ORDER BY users.usergroup ASC, users.lastname ASC';
198
        $req = $this->Db->prepare($sql);
199
        $req->bindValue(':query', '%' . $query . '%');
200
        if ($teamFilter) {
201
            $req->bindValue(':team', $this->userData['team']);
202
        }
203
        $this->Db->execute($req);
204
205
        $res = $req->fetchAll();
206
        if ($res === false) {
207
            return array();
208
        }
209
        return $res;
210
    }
211
212
    /**
213
     * Read all users from the team
214
     */
215
    public function readAllFromTeam(): array
216
    {
217
        return $this->readFromQuery('', true);
218
    }
219
220
    public function getLockedUsersCount(): int
221
    {
222
        $sql = 'SELECT COUNT(userid) FROM users WHERE allow_untrusted = 0';
223
        $req = $this->Db->prepare($sql);
224
        $this->Db->execute($req);
225
        return (int) $req->fetchColumn();
226
    }
227
228
    /**
229
     * Update user from the editusers template
230
     *
231
     * @param array<string, mixed> $params POST
232
     */
233
    public function update(array $params): bool
234
    {
235
        $this->checkEmail($params['email']);
236
237
        $firstname = Filter::sanitize($params['firstname']);
238
        $lastname = Filter::sanitize($params['lastname']);
239
240
        // (Sys)admins can only disable 2FA
241
        // input is disabled if there is no mfa active so no need for an else case
242
        $mfaSql = '';
243
        if ($params['use_mfa'] === 'off') {
244
            $mfaSql = ', mfa_secret = null';
245
        }
246
247
        $validated = 0;
248
        if ($params['validated'] === '1') {
249
            $validated = 1;
250
        }
251
252
        $usergroup = Check::id((int) $params['usergroup']);
253
254
        if (mb_strlen($params['password']) > 1) {
255
            $this->updatePassword($params['password']);
256
        }
257
258
        $sql = 'UPDATE users SET
259
            firstname = :firstname,
260
            lastname = :lastname,
261
            email = :email,
262
            usergroup = :usergroup,
263
            validated = :validated';
264
        $sql .= $mfaSql;
265
        $sql .= ' WHERE userid = :userid';
266
        $req = $this->Db->prepare($sql);
267
        $req->bindParam(':firstname', $firstname);
268
        $req->bindParam(':lastname', $lastname);
269
        $req->bindParam(':email', $params['email']);
270
        $req->bindParam(':usergroup', $usergroup);
271
        $req->bindParam(':validated', $validated);
272
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
273
        return $this->Db->execute($req);
274
    }
275
276
    /**
277
     * Update things from UCP
278
     *
279
     * @param array<string, mixed> $params
280
     */
281
    public function updateAccount(array $params): bool
282
    {
283
        $this->checkEmail($params['email']);
284
285
        $params['firstname'] = Filter::sanitize($params['firstname']);
286
        $params['lastname'] = Filter::sanitize($params['lastname']);
287
288
        // Check phone
289
        $params['phone'] = filter_var($params['phone'], FILTER_SANITIZE_STRING);
290
        // Check cellphone
291
        $params['cellphone'] = filter_var($params['cellphone'], FILTER_SANITIZE_STRING);
292
        // Check skype
293
        $params['skype'] = filter_var($params['skype'], FILTER_SANITIZE_STRING);
294
295
        // Check website
296
        $params['website'] = filter_var($params['website'], FILTER_VALIDATE_URL);
297
298
        $sql = 'UPDATE users SET
299
            email = :email,
300
            firstname = :firstname,
301
            lastname = :lastname,
302
            phone = :phone,
303
            cellphone = :cellphone,
304
            skype = :skype,
305
            website = :website
306
            WHERE userid = :userid';
307
        $req = $this->Db->prepare($sql);
308
309
        $req->bindParam(':email', $params['email']);
310
        $req->bindParam(':firstname', $params['firstname']);
311
        $req->bindParam(':lastname', $params['lastname']);
312
        $req->bindParam(':phone', $params['phone']);
313
        $req->bindParam(':cellphone', $params['cellphone']);
314
        $req->bindParam(':skype', $params['skype']);
315
        $req->bindParam(':website', $params['website']);
316
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
317
        return $this->Db->execute($req);
318
    }
319
320
    /**
321
     * Update the password for the user
322
     */
323
    public function updatePassword(string $password): bool
324
    {
325
        Check::passwordLength($password);
326
327
        $passwordHash = password_hash($password, PASSWORD_DEFAULT);
328
329
        $sql = 'UPDATE users SET password_hash = :password_hash, token = null WHERE userid = :userid';
330
        $req = $this->Db->prepare($sql);
331
        $req->bindParam(':password_hash', $passwordHash);
332
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
333
        return $this->Db->execute($req);
334
    }
335
336
    /**
337
     * Invalidate token on logout action
338
     */
339
    public function invalidateToken(): bool
340
    {
341
        $sql = 'UPDATE users SET token = null WHERE userid = :userid';
342
        $req = $this->Db->prepare($sql);
343
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
344
        return $this->Db->execute($req);
345
    }
346
347
    /**
348
     * Validate current user instance
349
     */
350
    public function validate(): bool
351
    {
352
        $sql = 'UPDATE users SET validated = 1 WHERE userid = :userid';
353
        $req = $this->Db->prepare($sql);
354
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
355
        return $this->Db->execute($req);
356
    }
357
358
    /**
359
     * Archive/Unarchive a user
360
     */
361
    public function toggleArchive(): bool
362
    {
363
        $sql = 'UPDATE users SET archived = IF(archived = 1, 0, 1), token = null WHERE userid = :userid';
364
        $req = $this->Db->prepare($sql);
365
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
366
        return $this->Db->execute($req);
367
    }
368
369
    /**
370
     * Lock all the experiments owned by user
371
     */
372
    public function lockExperiments(): bool
373
    {
374
        $sql = 'UPDATE experiments
375
            SET locked = :locked, lockedby = :userid, lockedwhen = CURRENT_TIMESTAMP WHERE userid = :userid';
376
        $req = $this->Db->prepare($sql);
377
        $req->bindValue(':locked', 1);
378
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
379
        return $this->Db->execute($req);
380
    }
381
382
    public function allowUntrustedLogin(): bool
383
    {
384
        $sql = 'SELECT allow_untrusted, auth_lock_time > (NOW() - INTERVAL 1 HOUR) AS currently_locked FROM users WHERE userid = :userid';
385
        $req = $this->Db->prepare($sql);
386
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
387
        $req->execute();
388
        $res = $req->fetch();
389
390
        if ($res['allow_untrusted'] === '1') {
391
            return true;
392
        }
393
        // check for the time when it was locked
394
        return $res['currently_locked'] === '0';
395
    }
396
397
    /**
398
     * Destroy user. Will completely remove everything from the user.
399
     */
400
    public function destroy(): bool
401
    {
402
        $UsersHelper = new UsersHelper((int) $this->userData['userid']);
403
        if ($UsersHelper->hasExperiments()) {
404 1
            throw new ImproperActionException('Cannot delete a user that owns experiments!');
405
        }
406
        $sql = 'DELETE FROM users WHERE userid = :userid';
407 1
        $req = $this->Db->prepare($sql);
408
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
409
        return $this->Db->execute($req);
410 1
    }
411 1
412 1
    /**
413
     * Get info about a user
414
     */
415
    private function getUserData(int $userid): array
416
    {
417 1
        $sql = "SELECT users.*, CONCAT(users.firstname, ' ', users.lastname) AS fullname,
418 1
            groups.can_lock, groups.is_admin, groups.is_sysadmin FROM users
419
            LEFT JOIN `groups` ON groups.id = users.usergroup
420
            WHERE users.userid = :userid";
421
        $req = $this->Db->prepare($sql);
422
        $req->bindParam(':userid', $userid, PDO::PARAM_INT);
423 1
        $this->Db->execute($req);
424
        $res = $req->fetch();
425
        if ($res === false) {
426
            throw new ResourceNotFoundException();
427 1
        }
428 1
429
        return $res;
430
    }
431 1
432 1
    private function checkEmail(string $email): void
433
    {
434
        // do nothing if the email sent is the same as the existing one
435 1
        if ($email === $this->userData['email']) {
436 1
            return;
437
        }
438
        // if the sent email is different from the existing one, check it's valid (not duplicate and respects domain constraint)
439 1
        $Config = Config::getConfig();
440 1
        $EmailValidator = new EmailValidator($email, $Config->configArr['email_domain']);
441
        $EmailValidator->validate();
442
    }
443
}
444