UserController.php 42.9 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

            $users[$key]['groups'] = [];
            $groupsIds = UserGroupModel::get(['select' => ['group_id'], 'where' => ['user_id = ?'], 'data' => [$user['id']]]);
            $groupsIds  = array_column($groupsIds, 'group_id');
82
83
84
85
            $actuallyAlone = false;
            if (empty($groupsIds)) {
                $actuallyAlone = true;
            } elseif ($GLOBALS['id'] != $user['id']) {
86
87
88
89
90
                $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;
91
            } elseif (!$actuallyAlone) {
92
                unset($users[$key]);
93
            }
94
95
        }

96
        return $response->withJson(['users' => array_values($users)]);
Damien's avatar
Damien committed
97
98
    }

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

105
106
107
        if ($GLOBALS['id'] == $args['id'] || PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
            $user = UserController::getUserInformationsById(['id' => $args['id']]);
        } else {
108
            $user = UserModel::getById(['select' => ['id', 'firstname', 'lastname', 'email', 'phone', 'substitute'], 'id' => $args['id']]);
109
        }
110

111
112
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
113
        }
Florian Azizian's avatar
Florian Azizian committed
114

115
116
117
118
        $user['groups'] = [];
        $userGroups = UserGroupModel::get(['select' => ['group_id'], 'where' => ['user_id = ?'], 'data' => [$args['id']]]);
        $groupsIds  = array_column($userGroups, 'group_id');

119
120
121
122
        $actuallyAlone = false;
        if (empty($groupsIds)) {
            $actuallyAlone = true;
        } elseif ($GLOBALS['id'] != $args['id']) {
123
            $groupsIds = array_values(array_intersect($groupsIds, array_column(UserController::getManageableGroups(['userId' => $GLOBALS['id']]), 'id')));
124
125
126
127
128
        }

        if (!empty($groupsIds)) {
            $groups = GroupModel::get(['select' => ['label', 'id'], 'where' => ['id in (?)'], 'data' => [$groupsIds]]);
            $user['groups'] = $groups;
129
130
        } elseif (!$actuallyAlone) {
            return $response->withStatus(403)->withJson(['errors' => 'User out of perimeter']);
131
132
133
134
135
136
137
138
139
140
        }

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

Florian Azizian's avatar
Florian Azizian committed
141
        return $response->withJson(['user' => $user]);
142
143
    }

Florian Azizian's avatar
Florian Azizian committed
144
145
    public function create(Request $request, Response $response)
    {
146
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
147
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
Damien's avatar
Damien committed
148
149
        }

Damien's avatar
Damien committed
150
151
152
153
        $body = $request->getParsedBody();

        if (empty($body)) {
            return $response->withStatus(400)->withJson(['errors' => 'Body is not set or empty']);
154
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['login']) || !preg_match("/^[\w.@-]*$/", $body['login'])) {
155
            return $response->withStatus(400)->withJson(['errors' => 'Body login is empty, not a string or wrong formatted']);
156
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['firstname'])) {
Damien's avatar
Damien committed
157
            return $response->withStatus(400)->withJson(['errors' => 'Body firstname is empty or not a string']);
158
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['lastname'])) {
Damien's avatar
Damien committed
159
            return $response->withStatus(400)->withJson(['errors' => 'Body lastname is empty or not a string']);
160
        } 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
161
            return $response->withStatus(400)->withJson(['errors' => 'Body email is empty or not a valid email']);
162
163
        } elseif (!empty($body['x509Fingerprint']) && !Validator::stringType()->validate($body['x509Fingerprint'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body x509Fingerprint is not a string']);
164
165
        } 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
166
167
        }

168
        $body['groups'] = !empty($body['groups']) ? array_column($body['groups'], 'id') : [];
169
170
        $manageableGroups = array_column(UserController::getManageableGroups(['userId' => $GLOBALS['id']]), 'id');
        $body['groups'] = array_values(array_intersect($body['groups'], $manageableGroups));
171

172
        $body['login'] = strtolower($body['login']);
Damien's avatar
Damien committed
173
        $existingUser = UserModel::getByLogin(['login' => $body['login'], 'select' => [1]]);
Florian Azizian's avatar
Florian Azizian committed
174
        if (!empty($existingUser)) {
175
            return $response->withStatus(400)->withJson(['errors' => 'Login already exists', 'lang' => 'userLoginAlreadyExists']);
Florian Azizian's avatar
Florian Azizian committed
176
        }
Damien's avatar
Damien committed
177

178
        $body['x509_fingerprint'] = $body['x509Fingerprint'];
Florian Azizian's avatar
Florian Azizian committed
179

Damien's avatar
Damien committed
180
        if (!empty($body['isRest'])) {
181
            $body['"isRest"'] = true;
Florian Azizian's avatar
Florian Azizian committed
182
        }
183
184
185
186
        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
187

188
189
190
191
        if (!empty($body['signatureModes'])) {
            if (!Validator::arrayType()->validate($body['signatureModes'])) {
                return $response->withStatus(400)->withJson(['errors' => 'Body signatureModes is not an array']);
            } else {
192
                $body['signatureModes'] = array_unique($body['signatureModes']);
193
194
195
                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"]);
196
197
198
199
                    }
                }
            }
        } else {
200
            $body['signatureModes'] = ['stamp'];
201
202
203
        }
        $body['signatureModes'] = json_encode($body['signatureModes']);

Damien's avatar
Damien committed
204
        $id = UserModel::create($body);
Florian Azizian's avatar
Florian Azizian committed
205
206

        HistoryController::add([
Damien's avatar
Damien committed
207
208
209
210
            'code'          => 'OK',
            'objectType'    => 'users',
            'objectId'      => $id,
            'type'          => 'CREATION',
Damien's avatar
Damien committed
211
            'message'       => "{userAdded} : {$body['firstname']} {$body['lastname']}"
Florian Azizian's avatar
Florian Azizian committed
212
213
        ]);

214
215
216
217
        if (empty($body['isRest'])) {
            AuthenticationController::sendAccountActivationNotification(['userId' => $id, 'userEmail' => $body['email']]);
        }

Damien's avatar
Damien committed
218
        return $response->withJson(['id' => $id]);
Florian Azizian's avatar
Florian Azizian committed
219
220
    }

Damien's avatar
Damien committed
221
222
    public function update(Request $request, Response $response, array $args)
    {
223
224
        $connection = ConfigurationModel::getConnection();
        if (($GLOBALS['id'] != $args['id'] || $connection != 'default') && !PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
225
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
Damien's avatar
Damien committed
226
        }
227
228
229
        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }
Damien's avatar
Damien committed
230

Damien's avatar
Damien committed
231
        $body = $request->getParsedBody();
232

233
234
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
235
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['firstname'])) {
236
            return $response->withStatus(400)->withJson(['errors' => 'Body firstname is empty or not a string']);
237
        } elseif (!Validator::stringType()->notEmpty()->length(1, 128)->validate($body['lastname'])) {
238
            return $response->withStatus(400)->withJson(['errors' => 'Body lastname is empty or not a string']);
239
        } 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
240
            return $response->withStatus(400)->withJson(['errors' => 'Body email is empty or not a valid email']);
241
242
        } elseif (!empty($body['x509Fingerprint']) && !Validator::stringType()->validate($body['x509Fingerprint'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body x509Fingerprint is not a string']);
243
244
        } elseif (!Validator::arrayType()->each(Validator::intType())->validate($body['groups'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body groups is not an array of integers']);
Damien's avatar
Damien committed
245
246
        }

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

252
        $set = [
253
254
255
            'firstname'       => $body['firstname'],
            'lastname'        => $body['lastname'],
            'email'           => $body['email'],
Damien's avatar
Damien committed
256
            'phone'           => $body['phone'],
257
            'signature_modes' => []
258
259
        ];

260
261
262
263
264
        $currentUser = UserModel::getById(['select' => ['"isRest"'], 'id' => $GLOBALS['id']]);
        if ($currentUser['isRest']) {
            $set['x509_fingerprint'] = $body['x509Fingerprint'];
        }

265
266
        if (!empty($body['signatureModes'])) {
            if (!Validator::arrayType()->validate($body['signatureModes'])) {
267
                return $response->withStatus(400)->withJson(['errors' => 'Body signatureModes is not an array']);
268
            }
269
            $body['signatureModes'] = array_unique($body['signatureModes']);
270
271
272
273
            $modes = [];
            foreach ($body['signatureModes'] as $signatureMode) {
                if (SignatureController::isValidSignatureMode(['mode' => $signatureMode])) {
                    $modes[] = $signatureMode;
274
275
                }
            }
276
277
278
279
280
281
282
283
284
            $validModes = CoreConfigModel::getSignatureModes();
            $higherMode = 'stamp';
            foreach ($validModes as $validMode) {
                if (in_array($validMode['id'], $modes)) {
                    $higherMode = $validMode['id'];
                    break;
                }
            }

285
            WorkflowModel::update([
286
                'set'   => ['signature_mode' => $higherMode],
287
                'where' => ['user_id = ?', 'signature_mode not in (?)', 'process_date is null', 'signature_mode != ?'],
288
                'data'  => [$args['id'], $modes, $higherMode]
289
290
            ]);
            WorkflowTemplateItemModel::update([
291
                'set'   => ['signature_mode' => $higherMode],
292
                'where' => ['user_id = ?', 'signature_mode not in (?)', 'signature_mode != ?'],
293
                'data'  => [$args['id'], $modes, $higherMode]
294
295
            ]);

296
            $set['signature_modes'] = $modes;
297
298
299
        }
        $set['signature_modes'] = json_encode($set['signature_modes']);

Damien's avatar
Damien committed
300
301
302
303
304
        UserModel::update([
            'set'   => $set,
            'where' => ['id = ?'],
            'data'  => [$args['id']]
        ]);
Damien's avatar
Damien committed
305

306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
        if (!empty($body['groups']) && Validator::arrayType()->each(Validator::intType())->validate($body['groups'])) {
            /**
             * Alice wants to edit Bob’s groups.
             *
             * B: $body[groups]: groups Alice wants to apply to Bob
             * M: $GLOBALS[id] manageable groups: groups Alice has right on
             * C: current groups: Bob’s current groups
             *
             * given these, Bob’s new groups are the groups Alice asked for,
             * plus “Bob’s current groups except groups Alice has right on”:
             *
             * B union (C - M)
             */
            $targetCurrentGroups = UserGroupModel::get([
                'select' => ['group_id'],
                'where'  => ['user_id = ?'],
                'data'   => [$args['id']]
            ]);
            $targetCurrentGroups = !empty($targetCurrentGroups) ? array_column($targetCurrentGroups, 'group_id') : [];
            $manageableGroups = array_column(UserController::getManageableGroups(['userId' => $GLOBALS['id']]), 'id');
            $appliedGroups = array_unique(array_merge($body['groups'], array_diff($targetCurrentGroups, $manageableGroups)));
            UserGroupModel::setUserGroups(['userId' => $args['id'], 'groups' => $appliedGroups]);
        }
329

Damien's avatar
Damien committed
330
        HistoryController::add([
Damien's avatar
Damien committed
331
332
333
334
            'code'          => 'OK',
            'objectType'    => 'users',
            'objectId'      => $args['id'],
            'type'          => 'MODIFICATION',
Damien's avatar
Damien committed
335
            'message'       => "{userUpdated} : {$body['firstname']} {$body['lastname']}"
Damien's avatar
Damien committed
336
337
        ]);

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

341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
    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);
        }
363
364
365
366
        $picture  = base64_decode($body['picture']);
        $finfo    = new \finfo(FILEINFO_MIME_TYPE);
        $mimeType = $finfo->buffer($picture);
        $type     = explode('/', $mimeType);
367
368
369
370
371

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

372
373
        $imagick = new \Imagick();
        $imagick->readImageBlob(base64_decode($body['picture']));
374
375
376
        if (!empty($body['pictureOrientation'])) {
            $imagick->rotateImage(new \ImagickPixel(), $body['pictureOrientation']);
        }
377
        $imagick->thumbnailImage(100, null);
378
        $body['picture'] = base64_encode($imagick->getImagesBlob());
379
380

        $set = [
381
            'picture' => $infoContent . $body['picture']
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
        ];

        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']}"
        ]);

398
        return $response->withStatus(204);
399
400
    }

Damien's avatar
Damien committed
401
402
    public function delete(Request $request, Response $response, array $args)
    {
403
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users']) || $GLOBALS['id'] == $args['id']) {
Damien's avatar
Damien committed
404
405
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }
406
407
408
        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }
Damien's avatar
Damien committed
409

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

Damien's avatar
Damien committed
414
415
416
417
418
        $user = UserModel::getById(['id' => $args['id'], 'select' => ['firstname', 'lastname']]);
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

419
420
421
        $substitutedUsers = UserModel::get(['select' => ['id'], 'where' => ['substitute = ?'], 'data' => [$args['id']]]);
        $allSubstitutedUsers = array_column($substitutedUsers, 'id');

Damien's avatar
Damien committed
422
        $workflows = WorkflowModel::get([
423
424
            'select' => ['main_document_id'],
            'where'  => ['user_id = ?', "process_date IS NULL", "status IS NULL"],
425
426
427
            'data'   => [$args['id']]
        ]);

428
        $mainDocumentId = array_column($workflows, 'main_document_id');
Damien's avatar
Damien committed
429

430
431
432
433
434
435
        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
436

437
438
439
440
441
442
            $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
443

444
445
446
447
448
449
450
451
            $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']) {
452
                    EmailController::sendOrFillNotificationToTypist(['documentId' => $document['id'], 'senderId' => $GLOBALS['id'], 'recipientId' => $document['typist'], 'mode' => 'DEL']);
453
454
                }
                $previousDocumentId = $step['main_document_id'];
455
456
            }
        }
Damien's avatar
Damien committed
457
458
459
460
461
462

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

463
464
465
466
467
        $workflowTemplates = WorkflowTemplateModel::get([
            'select' => ['id'],
            'where'  => ['owner = ?'],
            'data'   => [$args['id']]
        ]);
Damien's avatar
Damien committed
468

469
470
471
472
473
474
475
476
477
        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
478
479
480
        UserModel::delete(['id' => $args['id']]);

        HistoryController::add([
481
482
483
484
485
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $args['id'],
            'type'       => 'SUPPRESSION',
            'message'    => "{userDeleted} : {$user['firstname']} {$user['lastname']}"
Damien's avatar
Damien committed
486
487
488
489
490
        ]);

        return $response->withStatus(204);
    }

Damien's avatar
Damien committed
491
492
    public function getPictureById(Request $request, Response $response, array $args)
    {
493
494
495
496
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        }

Damien's avatar
Damien committed
497
498
499
500
501
502
503
504
        $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
505
506
507
508
509
510
    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']);
        }

511
512
513
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users']) && $GLOBALS['id'] != $args['id']) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }
514
515
516
        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }
517

Damien's avatar
Damien committed
518
519
520
521
522
523
524
525
        $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']]);
    }

526
527
528
529
530
531
    public function updatePreferences(Request $request, Response $response, array $args)
    {
        if ($GLOBALS['id'] != $args['id']) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

532
533
        $notificationsIds = NotificationsScheduleModel::get(['select' => ['id']]);
        $notificationsIds = array_column($notificationsIds, 'id');
534

535
536
        $body = $request->getParsedBody();

537
538
539
        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'])) {
540
541
542
543
544
545
546
            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']);
547
548
        } elseif (!Validator::arrayType()->notEmpty()->validate($body['notifications'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Body notifications is empty or not an array']);
549
        } elseif (!isset($body['notifications']['instant']) || !Validator::boolType()->validate($body['notifications']['instant'])) {
550
            return $response->withStatus(400)->withJson(['errors' => 'Body notifications.instant is not a boolean']);
551
        } elseif (!isset($body['notifications']['summaries']) || !Validator::arrayType()->each(Validator::in($notificationsIds))->validate($body['notifications']['summaries'])) {
552
            return $response->withStatus(400)->withJson(['errors' => 'Body notifications.summaries is not an array or contains invalid IDs']);
553
        } elseif (!isset($body['signatureScaling']) || !Validator::oneOf(Validator::falseVal(), Validator::intVal()->between(10, 50))->validate($body['signatureScaling'])) {
554
            return $response->withStatus(400)->withJson(['errors' => 'Body signatureScaling is neither false nor an integer between 10 and 50']);
555
556
        }

Damien's avatar
Damien committed
557
558
559
560
561
        $user = UserModel::getById(['id' => $args['id'], 'select' => ['firstname', 'lastname']]);
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

562
        $body['notifications']['summaries'] = array_values(array_unique($body['notifications']['summaries']));
563
        $preferences = json_encode([
564
565
566
567
568
569
570
571
572
            'lang'             => $body['lang'],
            'writingMode'      => $body['writingMode'],
            'writingSize'      => $body['writingSize'],
            'writingColor'     => $body['writingColor'],
            'signatureScaling' => $body['signatureScaling'],
            'notifications'    => [
                'instant'      => $body['notifications']['instant'],
                'summaries'    => $body['notifications']['summaries'],
            ]
573
574
575
576
577
578
579
580
581
582
583
584
        ]);
        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([
585
586
587
588
589
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $args['id'],
            'type'       => 'MODIFICATION',
            'message'    => "{userUpdated} : {$user['firstname']} {$user['lastname']}"
590
591
592
593
594
        ]);

        return $response->withStatus(204);
    }

595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
    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([
638
639
640
641
642
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $args['id'],
            'type'       => 'MODIFICATION',
            'message'    => "{userUpdated} : {$user['firstname']} {$user['lastname']}"
643
644
645
646
647
        ]);

        return $response->withStatus(204);
    }

Damien's avatar
Damien committed
648
649
    public function updatePassword(Request $request, Response $response, array $args)
    {
650
651
652
653
        if (!Validator::intVal()->notEmpty()->validate($args['id'])) {
            return $response->withStatus(400)->withJson(['errors' => 'Route id is not an integer']);
        }

654
655
656
657
658
659
        $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
660
661
662
663
        if ($GLOBALS['id'] != $args['id']) {
            if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
                return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
            }
664
665
666
            if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
                return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
            }
Damien's avatar
Damien committed
667
668
        }

669
670
        $body = $request->getParsedBody();
        if (!Validator::stringType()->notEmpty()->validate($body['newPassword'])) {
Damien's avatar
Damien committed
671
            return $response->withStatus(400)->withJson(['errors' => 'Body newPassword is empty or not a string']);
672
        } elseif ($body['newPassword'] != $body['passwordConfirmation']) {
Damien's avatar
Damien committed
673
            return $response->withStatus(400)->withJson(['errors' => 'Body newPassword and passwordConfirmation must be identical']);
Damien's avatar
Damien committed
674
675
        }

Damien's avatar
Damien committed
676
        if ($user['isRest'] == false) {
677
            if (empty($body['currentPassword']) || !AuthenticationModel::authentication(['login' => $user['login'], 'password' => $body['currentPassword']])) {
678
                return $response->withStatus(401)->withJson(['errors' => 'Wrong Password', 'lang' => 'wrongCurrentPassword']);
Damien's avatar
Damien committed
679
680
            }
        }
681
        if (!PasswordController::isPasswordValid(['password' => $body['newPassword']])) {
Damien's avatar
Damien committed
682
            return $response->withStatus(400)->withJson(['errors' => 'Password does not match security criteria']);
683
        } elseif (!PasswordModel::isPasswordHistoryValid(['password' => $body['newPassword'], 'userId' => $args['id']])) {
684
            return $response->withStatus(400)->withJson(['errors' => 'Password has already been used', 'lang' => 'alreadyUsedPassword']);
Damien's avatar
Damien committed
685
686
        }

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

Damien's avatar
Damien committed
690
691
        $refreshToken = [];
        if ($GLOBALS['id'] == $args['id']) {
692
            $refreshJWT     = AuthenticationController::getRefreshJWT();
Damien's avatar
Damien committed
693
            $refreshToken[] = $refreshJWT;
694
695
            $response       = $response->withHeader('Token', AuthenticationController::getJWT());
            $response       = $response->withHeader('Refresh-Token', $refreshJWT);
Damien's avatar
Damien committed
696
697
698
699
700
701
702
703
        }

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

Damien's avatar
Damien committed
704
        HistoryController::add([
705
706
707
708
709
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $args['id'],
            'type'       => 'MODIFICATION',
            'message'    => '{userPasswordUpdated}'
Damien's avatar
Damien committed
710
711
        ]);

712
        return $response->withStatus(204);
Damien's avatar
Damien committed
713
714
    }

Damien's avatar
Damien committed
715
716
717
718
719
720
721
722
    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']);
        }

723
        $user = UserModel::getByLogin(['select' => ['id', 'email', 'preferences'], 'login' => strtolower($body['login'])]);
Damien's avatar
Damien committed
724
        if (empty($user)) {
725
            return $response->withStatus(204);
Damien's avatar
Damien committed
726
727
728
729
        }

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

730
        $resetToken = AuthenticationController::getResetJWT(['id' => $GLOBALS['id'], 'expirationTime' => 3600]);
Damien's avatar
Damien committed
731
        UserModel::update(['set' => ['reset_token' => $resetToken], 'where' => ['id = ?'], 'data' => [$user['id']]]);
Damien's avatar
Damien committed
732
733

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

736
        $url = ConfigurationModel::getApplicationUrl() . 'dist/update-password?token=' . $resetToken;
Damien's avatar
Damien committed
737
        EmailController::createEmail([
738
739
740
741
742
            'userId' => $user['id'],
            'data'   => [
                'sender'     => 'Notification',
                'recipients' => [$user['email']],
                'subject'    => $lang['notificationForgotPasswordSubject'],
743
                'body'       => $lang['notificationForgotPasswordBody'] . '<a href="' . $url . '">'.$url.'</a>' . $lang['notificationForgotPasswordFooter'],
744
                'isHtml'     => true
Damien's avatar
Damien committed
745
746
747
748
            ]
        ]);

        HistoryController::add([
749
750
751
752
753
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $user['id'],
            'type'       => 'MODIFICATION',
            'message'    => '{userPasswordForgotten}'
Damien's avatar
Damien committed
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
        ]);

        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
769
770
771
        try {
            $jwt = JWT::decode($body['token'], CoreConfigModel::getEncryptKey(), ['HS256']);
        } catch (\Exception $e) {
772
            return $response->withStatus(403)->withJson(['errors' => 'Invalid token', 'lang' => 'invalidToken']);
Damien's avatar
Damien committed
773
        }
Damien's avatar
Damien committed
774

Damien's avatar
Damien committed
775
        $user = UserModel::getById(['id' => $jwt->user->id, 'select' => ['id', 'reset_token']]);
Damien's avatar
Damien committed
776
777
778
779
        if (empty($user)) {
            return $response->withStatus(400)->withJson(['errors' => 'User does not exist']);
        }

Damien's avatar
Damien committed
780
        if ($body['token'] != $user['reset_token']) {
781
            return $response->withStatus(403)->withJson(['errors' => 'Invalid token', 'lang' => 'invalidToken']);
Damien's avatar
Damien committed
782
783
784
785
786
787
788
789
        }

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

        UserModel::update([
            'set' => [
790
791
792
793
                'password'                   => AuthenticationModel::getPasswordHash($body['password']),
                'password_modification_date' => 'CURRENT_TIMESTAMP',
                'reset_token'                => null,
                'refresh_token'              => '[]'
Damien's avatar
Damien committed
794
795
            ],
            'where' => ['id = ?'],
Damien's avatar
Damien committed
796
            'data'  => [$user['id']]
Damien's avatar
Damien committed
797
798
799
800
        ]);

        $GLOBALS['id'] = $user['id'];
        HistoryController::add([
801
802
803
804
805
            'code'       => 'OK',
            'objectType' => 'users',
            'objectId'   => $user['id'],
            'type'       => 'MODIFICATION',
            'message'    => '{userForgottenPasswordUpdated}'
Damien's avatar
Damien committed
806
807
808
809
810
        ]);

        return $response->withStatus(204);
    }

811
812
813
814
815
816
    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']);
        }

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

821
822
823
824
        if (!UserController::hasRightByUserId(['activeUserId' => $GLOBALS['id'], 'targetUserId' => $args['id']])) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

825
826
827
828
829
830
831
832
833
834
835
836
        $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);
    }

837
    public function getManageableGroupsOfCurrentUser(Request $request, Response $response)
838
839
840
841
842
843
844
    {
        if (!PrivilegeController::hasPrivilege(['userId' => $GLOBALS['id'], 'privilege' => 'manage_users'])) {
            return $response->withStatus(403)->withJson(['errors' => 'Privilege forbidden']);
        }

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

845
        return $response->withJson(['groups' => $manageableGroups]);
846
847
    }

Damien's avatar
Damien committed
848
849
850
851
852
    public static function getUserInformationsById(array $args)
    {
        ValidatorModel::notEmpty($args, ['id']);
        ValidatorModel::intVal($args, ['id']);

Damien's avatar
Damien committed
853
        $user = UserModel::getById(['select' => ['id', 'login', 'email', 'firstname', 'lastname', 'phone', 'picture', 'preferences', 'substitute', '"isRest"', 'signature_modes', 'x509_fingerprint'], 'id' => $args['id']]);
854
855
856
        if (empty($user)) {
            return [];
        }
857
858
        $user['signatureModes'] = json_decode($user['signature_modes'], true);
        unset($user['signature_modes']);
Damien's avatar
Damien committed
859

860
861
862
863
        $validSignatureModes = CoreConfigModel::getSignatureModes();
        $validSignatureModes = array_column($validSignatureModes, 'id');
        $user['signatureModes'] = array_reverse(array_values(array_intersect($validSignatureModes, $user['signatureModes'])));

864
        if (empty($user['picture'])) {
Damien's avatar
Damien committed
865
866
867
868
            $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
869
        if ($GLOBALS['id'] == $args['id']) {
870
871
            $user['preferences']                = json_decode($user['preferences'], true);
            $user['availableLanguages']         = LanguageController::getAvailableLanguages();
872
            $user['administrativePrivileges']   = PrivilegeController::getPrivilegesByUserId(['userId' => $args['id'], 'type' => 'admin']);
873
            $user['appPrivileges']              = PrivilegeController::getPrivilegesByUserId(['userId' => $args['id'], 'type' => 'simple']);
874
            if (!empty($user['substitute'])) {
Damien's avatar
Damien committed
875
                $user['substituteUser'] = UserModel::getLabelledUserById(['id' => $user['substitute']]);
876
            }
Damien's avatar
Damien committed
877
        }
Damien's avatar
Damien committed
878

879
880
881
882
883
884
        $currentUser = UserModel::getById(['select' => ['"isRest"'], 'id' => $GLOBALS['id']]);
        if ($currentUser['isRest']) {
            $user['x509Fingerprint'] = $user['x509_fingerprint'];
        }
        unset($user['x509_fingerprint']);

885
886
887
        $connection = ConfigurationModel::getConnection();
        $user['canSendActivationNotification'] = !$user['isRest'] && $connection == 'default';

Damien's avatar
Damien committed
888
889
        return $user;
    }
890
891
892
893
894
895
896
897
898

    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');
899
            return GroupModel::get();
900
901
902
903
904
905
        }

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

        $manageableGroups = [];
        foreach ($groups as $group) {
906
907
908
909
910
            $privilege = GroupPrivilegeModel::getPrivileges([
                'select' => ['parameters'],
                'where'  =>  ['group_id = ?', 'privilege = ?'],
                'data'   => [$group['group_id'], 'manage_users']
            ]);
911
912
913
914
            $parameters = empty($privilege[0]['parameters']) ? [] : json_decode($privilege[0]['parameters'], true);
            $currentGroups = $parameters['authorized'] ?? [];
            $manageableGroups = array_merge($manageableGroups, $currentGroups);
        }
915
        $manageableGroups = array_unique($manageableGroups);
916

917
918
919
920
        if (empty($manageableGroups)) {
            return [];
        }

921
922
923
924
925
926
        $manageableGroups = GroupModel::get([
            'where' => ['id in (?)'],
            'data'  => [$manageableGroups]
        ]);

        return $manageableGroups;
927
    }
928
929
930
931
932
933
934
935
936
937
938
939

    public static function hasRightByUserId(array $args)
    {
        ValidatorModel::notEmpty($args, ['activeUserId', 'targetUserId']);
        ValidatorModel::intVal($args, ['activeUserId', 'targetUserId']);

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

940
941
        $activeUserManageableGroups = array_column(UserController::getManageableGroups(['userId' => $args['activeUserId']]), 'id');
        return !empty($activeUserManageableGroups) && (empty($groupsIds) || !empty(array_intersect($groupsIds, $activeUserManageableGroups)));
942
    }
Florian Azizian's avatar
Florian Azizian committed
943
}