UserController.php 39 KB
Newer Older
Florian Azizian's avatar
Florian Azizian committed
1
2
3
<?php

/**
4
5
* Copyright Maarch since 2008 under license.
* See LICENSE.txt file at the root folder for more details.
Florian Azizian's avatar
Florian Azizian committed
6
7
8
9
10
11
12
13
14
15
16
* This file is part of Maarch software.
*
*/

/**
* @brief User Controller
* @author dev@maarch.org
*/

namespace User\controllers;

17
use Configuration\models\ConfigurationModel;
18
19
use Document\controllers\DigitalSignatureController;
use Document\models\DocumentModel;
Damien's avatar
Damien committed
20
use Email\controllers\EmailController;
Damien's avatar
Damien committed
21
use Firebase\JWT\JWT;
22
use Group\controllers\PrivilegeController;
23
use Group\models\GroupModel;
24
use Group\models\GroupPrivilegeModel;
Damien's avatar
Damien committed
25
use History\controllers\HistoryController;
Florian Azizian's avatar
Florian Azizian committed
26
27
28
use Respect\Validation\Validator;
use Slim\Http\Request;
use Slim\Http\Response;
Damien's avatar
Damien committed
29
use SrcCore\controllers\AuthenticationController;
Damien's avatar
Damien committed
30
use SrcCore\controllers\LanguageController;
Damien's avatar
Damien committed
31
32
use SrcCore\controllers\PasswordController;
use SrcCore\models\AuthenticationModel;
Damien's avatar
Damien committed
33
use SrcCore\models\CoreConfigModel;
34
use SrcCore\models\PasswordModel;
Damien's avatar
Damien committed
35
use SrcCore\models\ValidatorModel;
36
use User\models\SignatureModel;
37
use User\models\UserGroupModel;
Florian Azizian's avatar
Florian Azizian committed
38
use User\models\UserModel;
Damien's avatar
Damien committed
39
use Workflow\models\WorkflowModel;
40
use Workflow\models\WorkflowTemplateItemModel;
41
use Workflow\models\WorkflowTemplateModel;
42
use Notification\models\NotificationsScheduleModel;
Florian Azizian's avatar
Florian Azizian committed
43
44
45

class UserController
{
Damien's avatar
Damien committed
46
47
    public function get(Request $request, Response $response)
    {
Damien's avatar
Damien committed
48
        $queryParams = $request->getQueryParams();
Damien's avatar
Damien committed
49

Damien's avatar
Damien committed
50
        $select = ['id', 'firstname', 'lastname', 'email', 'phone', 'substitute', 'x509_fingerprint'];
Damien's avatar
Damien committed
51
52
53
54
55
        $where = [];
        $queryData = [];
        if (empty($queryParams['mode'])) {
            $where = ['"isRest" = ?'];
            $queryData = ['false'];
56
57
58
        }
        if (PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
            $select[] = 'login';
Damien's avatar
Damien committed
59
60
        }

Damien's avatar
Damien committed
61
        $users = UserModel::get([
62
            'select'    => $select,
63
            'where'     => $where,
Damien's avatar
Damien committed
64
            'data'      => $queryData,
Damien's avatar
Damien committed
65
            'orderBy'   => ['lastname', 'firstname']
Damien's avatar
Damien committed
66
67
        ]);

68
        $currentUser = UserModel::getById(['select' => ['"isRest"'], 'id' => $GLOBALS['id']]);
69
70
        $manageableGroups = UserController::getManageableGroups(['userId' => $GLOBALS['id']]);
        $manageableGroups = array_column($manageableGroups, 'id');
71

72
73
        foreach ($users as $key => $user) {
            $users[$key]['substitute'] = !empty($user['substitute']);
74
75
76
77
            if ($currentUser['isRest']) {
                $users[$key]['x509Fingerprint'] = $users[$key]['x509_fingerprint'];
            }
            unset($users[$key]['x509_fingerprint']);
78
79
80
81
82
83
84
85
86
87
88

            $users[$key]['groups'] = [];
            $groupsIds = UserGroupModel::get(['select' => ['group_id'], 'where' => ['user_id = ?'], 'data' => [$user['id']]]);
            $groupsIds  = array_column($groupsIds, 'group_id');
            if ($GLOBALS['id'] != $user['id']) {
                $groupsIds = array_values(array_intersect($groupsIds, $manageableGroups));
            }
            if (!empty($groupsIds)) {
                $groups = GroupModel::get(['select' => ['label', 'id'], 'where' => ['id in (?)'], 'data' => [$groupsIds]]);
                $users[$key]['groups'] = $groups;
            }
89
90
        }

Damien's avatar
Damien committed
91
92
93
        return $response->withJson(['users' => $users]);
    }

94
95
    public function getById(Request $request, Response $response, array $args)
    {
96
97
98
99
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        }

100
101
102
        if ($GLOBALS['id'] == $args['id'] || PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
            $user = UserController::getUserInformationsById(['id' => $args['id']]);
        } else {
103
            $user = UserModel::getById(['select' => ['id', 'firstname', 'lastname', 'email', 'phone', 'substitute'], 'id' => $args['id']]);
104
        }
105

106
107
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
108
        }
Florian Azizian's avatar
Florian Azizian committed
109

110
111
112
113
114
        $user['groups'] = [];
        $userGroups = UserGroupModel::get(['select' => ['group_id'], 'where' => ['user_id = ?'], 'data' => [$args['id']]]);
        $groupsIds  = array_column($userGroups, 'group_id');

        if ($GLOBALS['id'] != $args['id']) {
115
            $groupsIds = array_values(array_intersect($groupsIds, array_column(UserController::getManageableGroups(['userId' => $GLOBALS['id']]), 'id')));
116
117
118
119
120
        }

        if (!empty($groupsIds)) {
            $groups = GroupModel::get(['select' => ['label', 'id'], 'where' => ['id in (?)'], 'data' => [$groupsIds]]);
            $user['groups'] = $groups;
121
122
123
124
125
126
127
128
129
130
        }

        HistoryController::add([
            'code'          => 'OK',
            'objectType'    => 'users',
            'objectId'      => $args['id'],
            'type'          => 'VIEW',
            'message'       => "{userViewed} : {$user['firstname']} {$user['lastname']}"
        ]);

Florian Azizian's avatar
Florian Azizian committed
131
        return $response->withJson(['user' => $user]);
132
133
    }

Florian Azizian's avatar
Florian Azizian committed
134
135
    public function create(Request $request, Response $response)
    {
136
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
137
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
Damien's avatar
Damien committed
138
139
        }

Damien's avatar
Damien committed
140
141
142
143
        $body = $request->getParsedBody();

        if (empty($body)) {
            return $response->withStatus(400)->withJson(['errors' => 'Body is not set or empty']);
144
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['login']) || !preg_match("/^[\w.@-]*$/", $body['login'])) {
145
            return $response->withStatus(400)->withJson(['errors' => 'Body login is empty, not a string or wrong formatted']);
146
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['firstname'])) {
Damien's avatar
Damien committed
147
            return $response->withStatus(400)->withJson(['errors' => 'Body firstname is empty or not a string']);
148
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['lastname'])) {
Damien's avatar
Damien committed
149
            return $response->withStatus(400)->withJson(['errors' => 'Body lastname is empty or not a string']);
150
        } elseif (empty($body['email']) || !filter_var($body['email'], FILTER_VALIDATE_EMAIL) || !Validator::stringType()->notEmpty()->length(1, 128)->validate($body['email'])) {
Damien's avatar
Damien committed
151
            return $response->withStatus(400)->withJson(['errors' => 'Body email is empty or not a valid email']);
152
153
        } elseif (!empty($body['x509Fingerprint']) && !Validator::stringType()->validate($body['x509Fingerprint'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body x509Fingerprint is not a string']);
154
155
        } elseif (!empty($body['groups']) && !Validator::arrayType()->validate($body['groups'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body groups is not an array']);
Florian Azizian's avatar
Florian Azizian committed
156
157
        }

158
        $body['groups'] = !empty($body['groups']) ? array_column($body['groups'], 'id') : [];
159

160
        $body['login'] = strtolower($body['login']);
Damien's avatar
Damien committed
161
        $existingUser = UserModel::getByLogin(['login' => $body['login'], 'select' => [1]]);
Florian Azizian's avatar
Florian Azizian committed
162
        if (!empty($existingUser)) {
163
            return $response->withStatus(400)->withJson(['errors' => 'Login already exists', 'lang' => 'userLoginAlreadyExists']);
Florian Azizian's avatar
Florian Azizian committed
164
        }
Damien's avatar
Damien committed
165

166
        $body['x509_fingerprint'] = $body['x509Fingerprint'];
Florian Azizian's avatar
Florian Azizian committed
167

Damien's avatar
Damien committed
168
        if (!empty($body['isRest'])) {
169
            $body['"isRest"'] = true;
Florian Azizian's avatar
Florian Azizian committed
170
        }
171
172
173
174
        if (empty($body['picture'])) {
            $body['picture'] = base64_encode(file_get_contents('src/frontend/assets/user_picture.png'));
            $body['picture'] = 'data:image/png;base64,' . $body['picture'];
        }
Florian Azizian's avatar
Florian Azizian committed
175

176
177
178
179
        if (!empty($body['signatureModes'])) {
            if (!Validator::arrayType()->validate($body['signatureModes'])) {
                return $response->withStatus(400)->withJson(['errors' => 'Body signatureModes is not an array']);
            } else {
180
                $body['signatureModes'] = array_unique($body['signatureModes']);
181
182
183
                foreach ($body['signatureModes'] as $key => $signatureMode) {
                    if (!SignatureController::isValidSignatureMode(['mode' => $signatureMode])) {
                        return $response->withStatus(400)->withJson(['errors' => "Body signatureModes[{$key}] is not a valid signature mode"]);
184
185
186
187
                    }
                }
            }
        } else {
188
            $body['signatureModes'] = ['stamp'];
189
190
191
        }
        $body['signatureModes'] = json_encode($body['signatureModes']);

Damien's avatar
Damien committed
192
        $id = UserModel::create($body);
Florian Azizian's avatar
Florian Azizian committed
193
194

        HistoryController::add([
Damien's avatar
Damien committed
195
196
197
198
            'code'          => 'OK',
            'objectType'    => 'users',
            'objectId'      => $id,
            'type'          => 'CREATION',
Damien's avatar
Damien committed
199
            'message'       => "{userAdded} : {$body['firstname']} {$body['lastname']}"
Florian Azizian's avatar
Florian Azizian committed
200
201
        ]);

202
203
204
205
        if (empty($body['isRest'])) {
            AuthenticationController::sendAccountActivationNotification(['userId' => $id, 'userEmail' => $body['email']]);
        }

Damien's avatar
Damien committed
206
        return $response->withJson(['id' => $id]);
Florian Azizian's avatar
Florian Azizian committed
207
208
    }

Damien's avatar
Damien committed
209
210
    public function update(Request $request, Response $response, array $args)
    {
211
212
        $connection = ConfigurationModel::getConnection();
        if (($GLOBALS['id'] != $args['id'] || $connection != 'default') && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
213
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
Damien's avatar
Damien committed
214
215
        }

Damien's avatar
Damien committed
216
        $body = $request->getParsedBody();
217

218
219
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
220
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['firstname'])) {
221
            return $response->withStatus(400)->withJson(['errors' => 'Body firstname is empty or not a string']);
222
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['lastname'])) {
223
            return $response->withStatus(400)->withJson(['errors' => 'Body lastname is empty or not a string']);
224
        } elseif (empty($body['email']) || !filter_var($body['email'], FILTER_VALIDATE_EMAIL) || !Validator::stringType()->notEmpty()->length(1, 128)->validate($body['email'])) {
Damien's avatar
Damien committed
225
            return $response->withStatus(400)->withJson(['errors' => 'Body email is empty or not a valid email']);
226
227
        } elseif (!empty($body['x509Fingerprint']) && !Validator::stringType()->validate($body['x509Fingerprint'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body x509Fingerprint is not a string']);
Damien's avatar
Damien committed
228
229
        }

230
        $user = UserModel::getById(['id' => $args['id'], 'select' => [1]]);
231
232
233
234
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

235
        $set = [
236
237
238
            'firstname'       => $body['firstname'],
            'lastname'        => $body['lastname'],
            'email'           => $body['email'],
Damien's avatar
Damien committed
239
            'phone'           => $body['phone'],
240
            'signature_modes' => []
241
242
        ];

243
244
245
246
247
        $currentUser = UserModel::getById(['select' => ['"isRest"'], 'id' => $GLOBALS['id']]);
        if ($currentUser['isRest']) {
            $set['x509_fingerprint'] = $body['x509Fingerprint'];
        }

248
249
        if (!empty($body['signatureModes'])) {
            if (!Validator::arrayType()->validate($body['signatureModes'])) {
250
                return $response->withStatus(400)->withJson(['errors' => 'Body signatureModes is not an array']);
251
            }
252
            $body['signatureModes'] = array_unique($body['signatureModes']);
253
254
255
256
            $modes = [];
            foreach ($body['signatureModes'] as $signatureMode) {
                if (SignatureController::isValidSignatureMode(['mode' => $signatureMode])) {
                    $modes[] = $signatureMode;
257
258
                }
            }
259
260
261
262
263
264
265
266
267
            $validModes = CoreConfigModel::getSignatureModes();
            $higherMode = 'stamp';
            foreach ($validModes as $validMode) {
                if (in_array($validMode['id'], $modes)) {
                    $higherMode = $validMode['id'];
                    break;
                }
            }

268
            WorkflowModel::update([
269
                'set'   => ['signature_mode' => $higherMode],
270
                'where' => ['user_id = ?', 'signature_mode not in (?)', 'process_date is null', 'signature_mode != ?'],
271
                'data'  => [$args['id'], $modes, $higherMode]
272
273
            ]);
            WorkflowTemplateItemModel::update([
274
                'set'   => ['signature_mode' => $higherMode],
275
                'where' => ['user_id = ?', 'signature_mode not in (?)', 'signature_mode != ?'],
276
                'data'  => [$args['id'], $modes, $higherMode]
277
278
            ]);

279
            $set['signature_modes'] = $modes;
280
281
282
        }
        $set['signature_modes'] = json_encode($set['signature_modes']);

Damien's avatar
Damien committed
283
284
285
286
287
        UserModel::update([
            'set'   => $set,
            'where' => ['id = ?'],
            'data'  => [$args['id']]
        ]);
Damien's avatar
Damien committed
288

Damien's avatar
Damien committed
289
        HistoryController::add([
Damien's avatar
Damien committed
290
291
292
293
            'code'          => 'OK',
            'objectType'    => 'users',
            'objectId'      => $args['id'],
            'type'          => 'MODIFICATION',
Damien's avatar
Damien committed
294
            'message'       => "{userUpdated} : {$body['firstname']} {$body['lastname']}"
Damien's avatar
Damien committed
295
296
        ]);

Damien's avatar
Damien committed
297
        return $response->withJson(['user' => UserController::getUserInformationsById(['id' => $args['id']])]);
Damien's avatar
Damien committed
298
299
    }

300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
    public function updatePicture(Request $request, Response $response, array $args)
    {
        if ($GLOBALS['id'] != $args['id']) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

        $body = $request->getParsedBody();

        if (!Validator::stringType()->notEmpty()->validate($body['picture'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body picture is empty']);
        }

        $user = UserModel::getById(['id' => $args['id'], 'select' => ['firstname', 'lastname']]);
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

        $infoContent = '';
        if (preg_match('/^data:image\/(\w+);base64,/', $body['picture'])) {
            $infoContent = substr($body['picture'], 0, strpos($body['picture'], ',') + 1);
            $body['picture'] = substr($body['picture'], strpos($body['picture'], ',') + 1);
        }
322
323
324
325
        $picture  = base64_decode($body['picture']);
        $finfo    = new \finfo(FILEINFO_MIME_TYPE);
        $mimeType = $finfo->buffer($picture);
        $type     = explode('/', $mimeType);
326
327
328
329
330

        if ($type[0] != 'image') {
            return $response->withStatus(400)->withJson(['errors' => 'Picture is not an image']);
        }

331
332
        $imagick = new \Imagick();
        $imagick->readImageBlob(base64_decode($body['picture']));
333
334
335
        if (!empty($body['pictureOrientation'])) {
            $imagick->rotateImage(new \ImagickPixel(), $body['pictureOrientation']);
        }
336
        $imagick->thumbnailImage(100, null);
337
        $body['picture'] = base64_encode($imagick->getImagesBlob());
338
339

        $set = [
340
            'picture' => $infoContent . $body['picture']
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
        ];

        UserModel::update([
            'set'   => $set,
            'where' => ['id = ?'],
            'data'  => [$args['id']]
        ]);

        HistoryController::add([
            'code'          => 'OK',
            'objectType'    => 'users',
            'objectId'      => $args['id'],
            'type'          => 'MODIFICATION',
            'message'       => "{userUpdated} : {$user['firstname']} {$user['lastname']}"
        ]);

357
        return $response->withStatus(204);
358
359
    }

Damien's avatar
Damien committed
360
361
    public function delete(Request $request, Response $response, array $args)
    {
362
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users']) || $GLOBALS['id'] == $args['id']) {
Damien's avatar
Damien committed
363
364
365
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

366
367
368
369
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        }

Damien's avatar
Damien committed
370
371
372
373
374
        $user = UserModel::getById(['id' => $args['id'], 'select' => ['firstname', 'lastname']]);
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

375
376
377
        $substitutedUsers = UserModel::get(['select' => ['id'], 'where' => ['substitute = ?'], 'data' => [$args['id']]]);
        $allSubstitutedUsers = array_column($substitutedUsers, 'id');

Damien's avatar
Damien committed
378
        $workflows = WorkflowModel::get([
379
380
            'select' => ['main_document_id'],
            'where'  => ['user_id = ?', "process_date IS NULL", "status IS NULL"],
381
382
383
            'data'   => [$args['id']]
        ]);

384
        $mainDocumentId = array_column($workflows, 'main_document_id');
Damien's avatar
Damien committed
385

386
387
388
389
390
391
        if (!empty($mainDocumentId)) {
            $workflows = WorkflowModel::get([
                'select' => ['id', 'digital_signature_id', 'main_document_id'],
                'where'  => ['main_document_id in (?)', "process_date IS NULL", "status IS NULL"],
                'data'   => [$mainDocumentId]
            ]);
Damien's avatar
Damien committed
392

393
394
395
396
397
398
            $workflowsId = array_column($workflows, 'id');
            WorkflowModel::update([
                'set'   => ['status' => 'STOP', 'process_date' => 'CURRENT_TIMESTAMP'],
                'where' => ['id in (?)', "process_date IS NULL AND status IS NULL"],
                'data'  => [$workflowsId]
            ]);
Damien's avatar
Damien committed
399

400
401
402
403
404
405
406
407
            $previousDocumentId = null;
            foreach ($workflows as $step) {
                $document = DocumentModel::getById(['select' => ['typist', 'id'], 'id' => $step['main_document_id']]);
                if (!empty($step['digital_signature_id'])) {
                    DigitalSignatureController::abort(['signatureId' => $step['digital_signature_id'], 'documentId' => $args['id']]);
                    break;
                }
                if ($previousDocumentId != $step['main_document_id']) {
408
                    EmailController::sendOrFillNotificationToTypist(['documentId' => $document['id'], 'senderId' => $GLOBALS['id'], 'recipientId' => $document['typist'], 'mode' => 'DEL']);
409
410
                }
                $previousDocumentId = $step['main_document_id'];
411
412
            }
        }
Damien's avatar
Damien committed
413
414
415
416
417
418

        //Substituted Users
        if (!empty($allSubstitutedUsers)) {
            UserModel::update(['set' => ['substitute' => null], 'where' => ['id in (?)'], 'data' => [$allSubstitutedUsers]]);
        }

419
420
421
422
423
        $workflowTemplates = WorkflowTemplateModel::get([
            'select' => ['id'],
            'where'  => ['owner = ?'],
            'data'   => [$args['id']]
        ]);
Damien's avatar
Damien committed
424

425
426
427
428
429
430
431
432
433
        if (!empty($workflowTemplates)) {
            $workflowTemplates = array_column($workflowTemplates, 'id');
            WorkflowTemplateItemModel::delete(['where' => ['workflow_template_id in (?)'], 'data' => [$workflowTemplates]]);
            WorkflowTemplateModel::delete(['where' => ['owner = ?'], 'data'  => [$args['id']]]);
        }

        SignatureModel::delete(['where' => ['user_id = ?'], 'data' => [$args['id']]]);
        UserGroupModel::delete(['where' => ['user_id = ?'], 'data' => [$args['id']]]);
        WorkflowTemplateItemModel::delete(['where' => ['user_id = ?'], 'data' => [$args['id']]]);
Damien's avatar
Damien committed
434
435
436
        UserModel::delete(['id' => $args['id']]);

        HistoryController::add([
437
438
439
440
441
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $args['id'],
            'type'       => 'SUPPRESSION',
            'message'    => "{userDeleted} : {$user['firstname']} {$user['lastname']}"
Damien's avatar
Damien committed
442
443
444
445
446
        ]);

        return $response->withStatus(204);
    }

Damien's avatar
Damien committed
447
448
    public function getPictureById(Request $request, Response $response, array $args)
    {
449
450
451
452
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        }

Damien's avatar
Damien committed
453
454
455
456
457
458
459
460
        $user = UserModel::getById(['select' => ['picture'], 'id' => $args['id']]);
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

        return $response->withJson(['picture' => $user['picture']]);
    }

Damien's avatar
Damien committed
461
462
463
464
465
466
    public function getSubstituteById(Request $request, Response $response, array $args)
    {
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        }

467
468
469
470
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users']) && $GLOBALS['id'] != $args['id']) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

Damien's avatar
Damien committed
471
472
473
474
475
476
477
478
        $user = UserModel::getById(['select' => ['substitute'], 'id' => $args['id']]);
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

        return $response->withJson(['substitute' => $user['substitute']]);
    }

479
480
481
482
483
484
    public function updatePreferences(Request $request, Response $response, array $args)
    {
        if ($GLOBALS['id'] != $args['id']) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

485
486
        $notificationsIds = NotificationsScheduleModel::get(['select' => ['id']]);
        $notificationsIds = array_column($notificationsIds, 'id');
487

488
489
        $body = $request->getParsedBody();

490
491
492
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        } elseif (!Validator::stringType()->notEmpty()->validate($body['lang'])) {
493
494
495
496
497
498
499
            return $response->withStatus(400)->withJson(['errors' => 'Body lang is empty or not a string']);
        } elseif (!Validator::stringType()->notEmpty()->validate($body['writingMode'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body writingMode is empty or not a string']);
        } elseif (!Validator::intType()->notEmpty()->validate($body['writingSize'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body writingSize is empty or not an integer']);
        } elseif (!Validator::stringType()->notEmpty()->validate($body['writingColor'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body writingColor is empty or not a string']);
500
501
        } elseif (!Validator::arrayType()->notEmpty()->validate($body['notifications'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body notifications is empty or not an array']);
502
        } elseif (!isset($body['notifications']['instant']) || !Validator::boolType()->validate($body['notifications']['instant'])) {
503
            return $response->withStatus(400)->withJson(['errors' => 'Body notifications.instant is not a boolean']);
504
        } elseif (!isset($body['notifications']['summaries']) || !Validator::arrayType()->each(Validator::in($notificationsIds))->validate($body['notifications']['summaries'])) {
505
            return $response->withStatus(400)->withJson(['errors' => 'Body notifications.summaries is not an array or contains invalid IDs']);
506
        } elseif (!isset($body['signatureScaling']) || !Validator::oneOf(Validator::falseVal(), Validator::intVal()->between(10, 50))->validate($body['signatureScaling'])) {
507
            return $response->withStatus(400)->withJson(['errors' => 'Body signatureScaling is neither false nor an integer between 10 and 50']);
508
509
        }

Damien's avatar
Damien committed
510
511
512
513
514
        $user = UserModel::getById(['id' => $args['id'], 'select' => ['firstname', 'lastname']]);
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

515
        $body['notifications']['summaries'] = array_values(array_unique($body['notifications']['summaries']));
516
        $preferences = json_encode([
517
518
519
520
521
522
523
524
525
            'lang'             => $body['lang'],
            'writingMode'      => $body['writingMode'],
            'writingSize'      => $body['writingSize'],
            'writingColor'     => $body['writingColor'],
            'signatureScaling' => $body['signatureScaling'],
            'notifications'    => [
                'instant'      => $body['notifications']['instant'],
                'summaries'    => $body['notifications']['summaries'],
            ]
526
527
528
529
530
531
532
533
534
535
536
537
        ]);
        if (!is_string($preferences)) {
            return $response->withStatus(400)->withJson(['errors' => 'Wrong format for user preferences data']);
        }

        UserModel::update([
            'set'   => ['preferences' => $preferences],
            'where' => ['id = ?'],
            'data'  => [$args['id']]
        ]);

        HistoryController::add([
538
539
540
541
542
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $args['id'],
            'type'       => 'MODIFICATION',
            'message'    => "{userUpdated} : {$user['firstname']} {$user['lastname']}"
543
544
545
546
547
        ]);

        return $response->withStatus(204);
    }

548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
    public function updateSubstitute(Request $request, Response $response, array $args)
    {
        if ($GLOBALS['id'] != $args['id']) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

        $body = $request->getParsedBody();

        $user = UserModel::getById(['id' => $args['id'], 'select' => ['firstname', 'lastname']]);
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

        $set = [
            'substitute' => null
        ];

        if (!empty($body['substitute']) && $args['id'] != $body['substitute']) {
            $existingUser = UserModel::getById(['id' => $body['substitute'], 'select' => ['substitute']]);
            if (empty($existingUser)) {
                return $response->withStatus(400)->withJson(['errors' => 'Substitute user does not exist']);
            } elseif (!empty($existingUser['substitute'])) {
                return $response->withStatus(400)->withJson(['errors' => 'Substitute user has already substituted']);
            }

            $substitutedUsers = UserModel::get(['select' => ['id'], 'where' => ['substitute = ?'], 'data' => [$args['id']]]);
            foreach ($substitutedUsers as $user) {
                UserModel::update([
                    'set'   => ['substitute' => $body['substitute']],
                    'where' => ['id = ?'],
                    'data'  => [$user['id']]
                ]);
            }
            $set['substitute'] = $body['substitute'];
        }

        UserModel::update([
            'set'   => $set,
            'where' => ['id = ?'],
            'data'  => [$args['id']]
        ]);

        HistoryController::add([
591
592
593
594
595
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $args['id'],
            'type'       => 'MODIFICATION',
            'message'    => "{userUpdated} : {$user['firstname']} {$user['lastname']}"
596
597
598
599
600
        ]);

        return $response->withStatus(204);
    }

Damien's avatar
Damien committed
601
602
    public function updatePassword(Request $request, Response $response, array $args)
    {
603
604
605
606
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        }

607
608
609
610
611
612
        $user = UserModel::getById(['select' => ['login', '"isRest"'], 'id' => $args['id']]);
        $connection = ConfigurationModel::getConnection();
        if ($connection != 'default' && $user['isRest'] == false) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

Damien's avatar
Damien committed
613
614
615
616
617
618
        if ($GLOBALS['id'] != $args['id']) {
            if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
                return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
            }
        }

619
620
        $body = $request->getParsedBody();
        if (!Validator::stringType()->notEmpty()->validate($body['newPassword'])) {
Damien's avatar
Damien committed
621
            return $response->withStatus(400)->withJson(['errors' => 'Body newPassword is empty or not a string']);
622
        } elseif ($body['newPassword'] != $body['passwordConfirmation']) {
Damien's avatar
Damien committed
623
            return $response->withStatus(400)->withJson(['errors' => 'Body newPassword and passwordConfirmation must be identical']);
Damien's avatar
Damien committed
624
625
        }

Damien's avatar
Damien committed
626
        if ($user['isRest'] == false) {
627
            if (empty($body['currentPassword']) || !AuthenticationModel::authentication(['login' => $user['login'], 'password' => $body['currentPassword']])) {
628
                return $response->withStatus(401)->withJson(['errors' => 'Wrong Password', 'lang' => 'wrongCurrentPassword']);
Damien's avatar
Damien committed
629
630
            }
        }
631
        if (!PasswordController::isPasswordValid(['password' => $body['newPassword']])) {
Damien's avatar
Damien committed
632
            return $response->withStatus(400)->withJson(['errors' => 'Password does not match security criteria']);
633
        } elseif (!PasswordModel::isPasswordHistoryValid(['password' => $body['newPassword'], 'userId' => $args['id']])) {
634
            return $response->withStatus(400)->withJson(['errors' => 'Password has already been used', 'lang' => 'alreadyUsedPassword']);
Damien's avatar
Damien committed
635
636
        }

637
        UserModel::updatePassword(['id' => $args['id'], 'password' => $body['newPassword']]);
638
        PasswordModel::setHistoryPassword(['userId' => $args['id'], 'password' => $body['newPassword']]);
Damien's avatar
Damien committed
639

Damien's avatar
Damien committed
640
641
        $refreshToken = [];
        if ($GLOBALS['id'] == $args['id']) {
642
            $refreshJWT     = AuthenticationController::getRefreshJWT();
Damien's avatar
Damien committed
643
            $refreshToken[] = $refreshJWT;
644
645
            $response       = $response->withHeader('Token', AuthenticationController::getJWT());
            $response       = $response->withHeader('Refresh-Token', $refreshJWT);
Damien's avatar
Damien committed
646
647
648
649
650
651
652
653
        }

        UserModel::update([
            'set'   => ['refresh_token' => json_encode($refreshToken)],
            'where' => ['id = ?'],
            'data'  => [$args['id']]
        ]);

Damien's avatar
Damien committed
654
        HistoryController::add([
655
656
657
658
659
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $args['id'],
            'type'       => 'MODIFICATION',
            'message'    => '{userPasswordUpdated}'
Damien's avatar
Damien committed
660
661
        ]);

662
        return $response->withStatus(204);
Damien's avatar
Damien committed
663
664
    }

Damien's avatar
Damien committed
665
666
667
668
669
670
671
672
    public function forgotPassword(Request $request, Response $response)
    {
        $body = $request->getParsedBody();

        if (!Validator::stringType()->notEmpty()->validate($body['login'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Bad request']);
        }

673
        $user = UserModel::getByLogin(['select' => ['id', 'email', 'preferences'], 'login' => strtolower($body['login'])]);
Damien's avatar
Damien committed
674
        if (empty($user)) {
675
            return $response->withStatus(204);
Damien's avatar
Damien committed
676
677
678
679
        }

        $GLOBALS['id'] = $user['id'];

680
        $resetToken = AuthenticationController::getResetJWT(['id' => $GLOBALS['id'], 'expirationTime' => 3600]);
Damien's avatar
Damien committed
681
        UserModel::update(['set' => ['reset_token' => $resetToken], 'where' => ['id = ?'], 'data' => [$user['id']]]);
Damien's avatar
Damien committed
682
683

        $user['preferences'] = json_decode($user['preferences'], true);
Damien's avatar
Damien committed
684
        $lang = LanguageController::get(['lang' => $user['preferences']['lang']]);
Damien's avatar
Damien committed
685

686
        $url = ConfigurationModel::getApplicationUrl() . 'dist/update-password?token=' . $resetToken;
Damien's avatar
Damien committed
687
        EmailController::createEmail([
688
689
690
691
692
            'userId' => $user['id'],
            'data'   => [
                'sender'     => 'Notification',
                'recipients' => [$user['email']],
                'subject'    => $lang['notificationForgotPasswordSubject'],
693
                'body'       => $lang['notificationForgotPasswordBody'] . '<a href="' . $url . '">'.$url.'</a>' . $lang['notificationForgotPasswordFooter'],
694
                'isHtml'     => true
Damien's avatar
Damien committed
695
696
697
698
            ]
        ]);

        HistoryController::add([
699
700
701
702
703
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $user['id'],
            'type'       => 'MODIFICATION',
            'message'    => '{userPasswordForgotten}'
Damien's avatar
Damien committed
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
        ]);

        return $response->withStatus(204);
    }

    public static function updateForgottenPassword(Request $request, Response $response)
    {
        $body = $request->getParsedBody();

        $check = Validator::stringType()->notEmpty()->validate($body['token']);
        $check = $check && Validator::stringType()->notEmpty()->validate($body['password']);
        if (!$check) {
            return $response->withStatus(400)->withJson(['errors' => 'Bad Request']);
        }

Damien's avatar
Damien committed
719
720
721
        try {
            $jwt = JWT::decode($body['token'], CoreConfigModel::getEncryptKey(), ['HS256']);
        } catch (\Exception $e) {
722
            return $response->withStatus(403)->withJson(['errors' => 'Invalid token', 'lang' => 'invalidToken']);
Damien's avatar
Damien committed
723
        }
Damien's avatar
Damien committed
724

Damien's avatar
Damien committed
725
        $user = UserModel::getById(['id' => $jwt->user->id, 'select' => ['id', 'reset_token']]);
Damien's avatar
Damien committed
726
727
728
729
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

Damien's avatar
Damien committed
730
        if ($body['token'] != $user['reset_token']) {
731
            return $response->withStatus(403)->withJson(['errors' => 'Invalid token', 'lang' => 'invalidToken']);
Damien's avatar
Damien committed
732
733
734
735
736
737
738
739
        }

        if (!PasswordController::isPasswordValid(['password' => $body['password']])) {
            return $response->withStatus(400)->withJson(['errors' => 'Password does not match security criteria']);
        }

        UserModel::update([
            'set' => [
740
741
742
743
                'password'                   => AuthenticationModel::getPasswordHash($body['password']),
                'password_modification_date' => 'CURRENT_TIMESTAMP',
                'reset_token'                => null,
                'refresh_token'              => '[]'
Damien's avatar
Damien committed
744
745
            ],
            'where' => ['id = ?'],
Damien's avatar
Damien committed
746
            'data'  => [$user['id']]
Damien's avatar
Damien committed
747
748
749
750
        ]);

        $GLOBALS['id'] = $user['id'];
        HistoryController::add([
751
752
753
754
755
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $user['id'],
            'type'       => 'MODIFICATION',
            'message'    => '{userForgottenPasswordUpdated}'
Damien's avatar
Damien committed
756
757
758
759
760
        ]);

        return $response->withStatus(204);
    }

761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
    public function sendAccountActivationNotification(Request $request, Response $response, array $args)
    {
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        }

        $connection = ConfigurationModel::getConnection();
        if ($connection != 'default') {
            return $response->withStatus(403)->withJson(['errors' => 'Cannot send activation notification when not using default connection']);
        }

        $user = UserModel::getById(['id' => $args['id'], 'select' => ['email']]);

        AuthenticationController::sendAccountActivationNotification(['userId' => $args['id'], 'userEmail' => $user['email']]);

        return $response->withStatus(204);
    }

783
784
785
786
787
788
789
790
    public function getManageableGroupsREST(Request $request, Response $response)
    {
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

        $manageableGroups = UserController::getManageableGroups(['userId' => $GLOBALS['id']]);

791
        return $response->withJson(['groups' => $manageableGroups]);
792
793
    }

Damien's avatar
Damien committed
794
795
796
797
798
    public static function getUserInformationsById(array $args)
    {
        ValidatorModel::notEmpty($args, ['id']);
        ValidatorModel::intVal($args, ['id']);

Damien's avatar
Damien committed
799
        $user = UserModel::getById(['select' => ['id', 'login', 'email', 'firstname', 'lastname', 'phone', 'picture', 'preferences', 'substitute', '"isRest"', 'signature_modes', 'x509_fingerprint'], 'id' => $args['id']]);
800
801
802
        if (empty($user)) {
            return [];
        }
803
804
        $user['signatureModes'] = json_decode($user['signature_modes'], true);
        unset($user['signature_modes']);
Damien's avatar
Damien committed
805

806
807
808
809
        $validSignatureModes = CoreConfigModel::getSignatureModes();
        $validSignatureModes = array_column($validSignatureModes, 'id');
        $user['signatureModes'] = array_reverse(array_values(array_intersect($validSignatureModes, $user['signatureModes'])));

810
        if (empty($user['picture'])) {
Damien's avatar
Damien committed
811
812
813
814
            $user['picture'] = base64_encode(file_get_contents('src/frontend/assets/user_picture.png'));
            $user['picture'] = 'data:image/png;base64,' . $user['picture'];
        }

Damien's avatar
Damien committed
815
        if ($GLOBALS['id'] == $args['id']) {
816
817
            $user['preferences']                = json_decode($user['preferences'], true);
            $user['availableLanguages']         = LanguageController::getAvailableLanguages();
818
            $user['administrativePrivileges']   = PrivilegeController::getPrivilegesByUserId(['userId' => $args['id'], 'type' => 'admin']);
819
            $user['appPrivileges']              = PrivilegeController::getPrivilegesByUserId(['userId' => $args['id'], 'type' => 'simple']);
820
            if (!empty($user['substitute'])) {
Damien's avatar
Damien committed
821
                $user['substituteUser'] = UserModel::getLabelledUserById(['id' => $user['substitute']]);
822
            }
Damien's avatar
Damien committed
823
        }
Damien's avatar
Damien committed
824

825
826
827
828
829
830
        $currentUser = UserModel::getById(['select' => ['"isRest"'], 'id' => $GLOBALS['id']]);
        if ($currentUser['isRest']) {
            $user['x509Fingerprint'] = $user['x509_fingerprint'];
        }
        unset($user['x509_fingerprint']);

831
832
833
        $connection = ConfigurationModel::getConnection();
        $user['canSendActivationNotification'] = !$user['isRest'] && $connection == 'default';

Damien's avatar
Damien committed
834
835
        return $user;
    }
836
837
838
839
840
841
842
843
844

    public static function getManageableGroups(array $args)
    {
        ValidatorModel::notEmpty($args, ['userId']);
        ValidatorModel::intVal($args, ['userId']);

        if (PrivilegeController::hasPrivilege(['userId' => $args['userId'], 'privilege' => 'manage_groups'])) {
            $groups = GroupModel::get(['select' => ['id']]);
            $groups = array_column($groups, 'id');
845
            return GroupModel::get();
846
847
848
849
850
851
        }

        $groups = UserGroupModel::get(['select' => ['group_id'], 'where' => ['user_id = ?'], 'data' => [$args['userId']]]);

        $manageableGroups = [];
        foreach ($groups as $group) {
852
853
854
855
856
            $privilege = GroupPrivilegeModel::getPrivileges([
                'select' => ['parameters'],
                'where'  =>  ['group_id = ?', 'privilege = ?'],
                'data'   => [$group['group_id'], 'manage_users']
            ]);
857
858
859
860
            $parameters = empty($privilege[0]['parameters']) ? [] : json_decode($privilege[0]['parameters'], true);
            $currentGroups = $parameters['authorized'] ?? [];
            $manageableGroups = array_merge($manageableGroups, $currentGroups);
        }
861
        $manageableGroups = array_unique($manageableGroups);
862

863
864
865
866
        if (empty($manageableGroups)) {
            return [];
        }

867
868
869
870
871
872
        $manageableGroups = GroupModel::get([
            'where' => ['id in (?)'],
            'data'  => [$manageableGroups]
        ]);

        return $manageableGroups;
873
    }
Florian Azizian's avatar
Florian Azizian committed
874
}