Passed
Push — hypernext ( bcec6c...a39b39 )
by Nico
10:59
created

Users::getUnarchivedCount()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 0
dl 0
loc 7
ccs 2
cts 3
cp 0.6667
crap 1.037
rs 10
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
        return $this->Db->fetchAll($req);
206
    }
207
208
    /**
209
     * Read all users from the team
210
     */
211
    public function readAllFromTeam(): array
212
    {
213
        return $this->readFromQuery('', true);
214
    }
215
216
    public function getLockedUsersCount(): int
217
    {
218
        $sql = 'SELECT COUNT(userid) FROM users WHERE allow_untrusted = 0';
219
        $req = $this->Db->prepare($sql);
220
        $this->Db->execute($req);
221
        return (int) $req->fetchColumn();
222
    }
223
224
    /**
225
     * Update user from the editusers template
226
     *
227
     * @param array<string, mixed> $params POST
228
     */
229
    public function update(array $params): bool
230
    {
231
        $this->checkEmail($params['email']);
232
233
        $firstname = Filter::sanitize($params['firstname']);
234
        $lastname = Filter::sanitize($params['lastname']);
235
236
        // (Sys)admins can only disable 2FA
237
        // input is disabled if there is no mfa active so no need for an else case
238
        $mfaSql = '';
239
        if ($params['use_mfa'] === 'off') {
240
            $mfaSql = ', mfa_secret = null';
241
        }
242
243
        $validated = 0;
244
        if ($params['validated'] === '1') {
245
            $validated = 1;
246
        }
247
248
        $usergroup = Check::id((int) $params['usergroup']);
249
250
        if (mb_strlen($params['password']) > 1) {
251
            $this->updatePassword($params['password']);
252
        }
253
254
        $sql = 'UPDATE users SET
255
            firstname = :firstname,
256
            lastname = :lastname,
257
            email = :email,
258
            usergroup = :usergroup,
259
            validated = :validated';
260
        $sql .= $mfaSql;
261
        $sql .= ' WHERE userid = :userid';
262
        $req = $this->Db->prepare($sql);
263
        $req->bindParam(':firstname', $firstname);
264
        $req->bindParam(':lastname', $lastname);
265
        $req->bindParam(':email', $params['email']);
266
        $req->bindParam(':usergroup', $usergroup);
267
        $req->bindParam(':validated', $validated);
268
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
269
        return $this->Db->execute($req);
270
    }
271
272
    /**
273
     * Update things from UCP
274
     *
275
     * @param array<string, mixed> $params
276
     */
277
    public function updateAccount(array $params): bool
278
    {
279
        $this->checkEmail($params['email']);
280
281
        $params['firstname'] = Filter::sanitize($params['firstname']);
282
        $params['lastname'] = Filter::sanitize($params['lastname']);
283
284
        // Check phone
285
        $params['phone'] = filter_var($params['phone'], FILTER_SANITIZE_STRING);
286
        // Check cellphone
287
        $params['cellphone'] = filter_var($params['cellphone'], FILTER_SANITIZE_STRING);
288
        // Check skype
289
        $params['skype'] = filter_var($params['skype'], FILTER_SANITIZE_STRING);
290
291
        // Check website
292
        $params['website'] = filter_var($params['website'], FILTER_VALIDATE_URL);
293
294
        $sql = 'UPDATE users SET
295
            email = :email,
296
            firstname = :firstname,
297
            lastname = :lastname,
298
            phone = :phone,
299
            cellphone = :cellphone,
300
            skype = :skype,
301
            website = :website
302
            WHERE userid = :userid';
303
        $req = $this->Db->prepare($sql);
304
305
        $req->bindParam(':email', $params['email']);
306
        $req->bindParam(':firstname', $params['firstname']);
307
        $req->bindParam(':lastname', $params['lastname']);
308
        $req->bindParam(':phone', $params['phone']);
309
        $req->bindParam(':cellphone', $params['cellphone']);
310
        $req->bindParam(':skype', $params['skype']);
311
        $req->bindParam(':website', $params['website']);
312
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
313
        return $this->Db->execute($req);
314
    }
315
316
    /**
317
     * Update the password for the user
318
     */
319
    public function updatePassword(string $password): bool
320
    {
321
        Check::passwordLength($password);
322
323
        $passwordHash = password_hash($password, PASSWORD_DEFAULT);
324
325
        $sql = 'UPDATE users SET password_hash = :password_hash, token = null WHERE userid = :userid';
326
        $req = $this->Db->prepare($sql);
327
        $req->bindParam(':password_hash', $passwordHash);
328
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
329
        return $this->Db->execute($req);
330
    }
331
332
    /**
333
     * Invalidate token on logout action
334
     */
335
    public function invalidateToken(): bool
336
    {
337
        $sql = 'UPDATE users SET token = null WHERE userid = :userid';
338
        $req = $this->Db->prepare($sql);
339
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
340
        return $this->Db->execute($req);
341
    }
342
343
    /**
344
     * Validate current user instance
345
     */
346
    public function validate(): bool
347
    {
348
        $sql = 'UPDATE users SET validated = 1 WHERE userid = :userid';
349
        $req = $this->Db->prepare($sql);
350
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
351
        return $this->Db->execute($req);
352
    }
353
354
    /**
355
     * Archive/Unarchive a user
356
     */
357
    public function toggleArchive(): bool
358
    {
359
        if ($this->userData['archived']) {
360
            if ($this->getUnarchivedCount() > 0) {
361
                throw new ImproperActionException('Cannot unarchive this user because they have another active account with the same email!');
362
            }
363
        }
364
365
        $sql = 'UPDATE users SET archived = IF(archived = 1, 0, 1), token = null WHERE userid = :userid';
366
        $req = $this->Db->prepare($sql);
367
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
368
        return $this->Db->execute($req);
369
    }
370
371
    /**
372
     * Lock all the experiments owned by user
373
     */
374
    public function lockExperiments(): bool
375
    {
376
        $sql = 'UPDATE experiments
377
            SET locked = :locked, lockedby = :userid, lockedwhen = CURRENT_TIMESTAMP WHERE userid = :userid';
378
        $req = $this->Db->prepare($sql);
379
        $req->bindValue(':locked', 1);
380
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
381
        return $this->Db->execute($req);
382
    }
383
384
    public function allowUntrustedLogin(): bool
385
    {
386
        $sql = 'SELECT allow_untrusted, auth_lock_time > (NOW() - INTERVAL 1 HOUR) AS currently_locked FROM users WHERE userid = :userid';
387
        $req = $this->Db->prepare($sql);
388
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
389
        $req->execute();
390
        $res = $req->fetch();
391
392
        if ($res['allow_untrusted'] === '1') {
393
            return true;
394
        }
395
        // check for the time when it was locked
396
        return $res['currently_locked'] === '0';
397
    }
398
399
    /**
400
     * Destroy user. Will completely remove everything from the user.
401
     */
402
    public function destroy(): bool
403
    {
404 1
        $UsersHelper = new UsersHelper((int) $this->userData['userid']);
405
        if ($UsersHelper->hasExperiments()) {
406
            throw new ImproperActionException('Cannot delete a user that owns experiments!');
407 1
        }
408
        $sql = 'DELETE FROM users WHERE userid = :userid';
409
        $req = $this->Db->prepare($sql);
410 1
        $req->bindParam(':userid', $this->userData['userid'], PDO::PARAM_INT);
411 1
        return $this->Db->execute($req);
412 1
    }
413
414
    // if the user is already archived, make sure there is no other account with the same email
415
    private function getUnarchivedCount(): int
416
    {
417 1
        $sql = 'SELECT COUNT(email) FROM users WHERE email = :email AND archived = 0';
418 1
        $req = $this->Db->prepare($sql);
419
        $req->bindParam(':email', $this->userData['email']);
420
        $this->Db->execute($req);
421
        return (int) $req->fetchColumn();
422
    }
423 1
424
    /**
425
     * Get info about a user
426
     */
427 1
    private function getUserData(int $userid): array
428 1
    {
429
        $sql = "SELECT users.*, CONCAT(users.firstname, ' ', users.lastname) AS fullname,
430
            groups.can_lock, groups.is_admin, groups.is_sysadmin FROM users
431 1
            LEFT JOIN `groups` ON groups.id = users.usergroup
432 1
            WHERE users.userid = :userid";
433
        $req = $this->Db->prepare($sql);
434
        $req->bindParam(':userid', $userid, PDO::PARAM_INT);
435 1
        $this->Db->execute($req);
436 1
        $res = $req->fetch();
437
        if ($res === false) {
438
            throw new ResourceNotFoundException();
439 1
        }
440 1
441
        return $res;
442
    }
443
444
    private function checkEmail(string $email): void
445 1
    {
446
        // do nothing if the email sent is the same as the existing one
447 1
        if ($email === $this->userData['email']) {
448
            return;
449 1
        }
450
        // if the sent email is different from the existing one, check it's valid (not duplicate and respects domain constraint)
451 1
        $Config = Config::getConfig();
452
        $EmailValidator = new EmailValidator($email, $Config->configArr['email_domain']);
453 1
        $EmailValidator->validate();
454 1
    }
455
}
456