Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
76.92% covered (warning)
76.92%
110 / 143
53.33% covered (warning)
53.33%
8 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Useraccount
76.92% covered (warning)
76.92%
110 / 143
53.33% covered (warning)
53.33%
8 / 15
42.81
0.00% covered (danger)
0.00%
0 / 1
 permissionExists
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
2.00
 getEntityMapping
100.00% covered (success)
100.00%
50 / 50
100.00% covered (success)
100.00%
1 / 1
1
 addConditionLoginName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionUserId
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionPassword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionXauthKey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 addConditionRoleLevel
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addConditionSearch
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 reverseEntityMapping
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 postProcess
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
7.01
 addConditionDepartmentIds
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 addConditionDepartmentIdsAndSearch
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 addConditionExcludeSuperusers
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 addOrderByName
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 addConditionWorkstationAccess
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace BO\Zmsdb\Query;
4
5use BO\Slim\Application as App;
6
7class Useraccount extends Base implements MappingInterface
8{
9    private const VALID_PERMISSION_NAMES = [
10        'appointment',
11        'availability',
12        'calldisplay',
13        'cherrypick',
14        'cluster',
15        'config',
16        'counter',
17        'customersearch',
18        'dayoff',
19        'department',
20        'emergency',
21        'finishedqueue',
22        'finishedqueuepast',
23        'logs',
24        'mailtemplates',
25        'missedqueue',
26        'openqueue',
27        'organisation',
28        'overviewcalendar',
29        'parkedqueue',
30        'restrictedscope',
31        'scope',
32        'source',
33        'statistic',
34        'ticketprinter',
35        'useraccount',
36        'waitingqueue',
37        'superuser',
38    ];
39
40    /**
41     * @var String TABLE mysql table reference
42     */
43    const TABLE = 'nutzer';
44    const TABLE_ASSIGNMENT = 'nutzerzuordnung';
45
46    const QUERY_READ_ID_BY_USERNAME = '
47        SELECT user.`NutzerID` AS id
48        FROM ' . self::TABLE . ' user
49        WHERE
50            user.`Name`=?
51    ';
52
53    const QUERY_WRITE_ASSIGNED_DEPARTMENTS = '
54        REPLACE INTO
55            ' . self::TABLE_ASSIGNMENT . '
56        SET
57            nutzerid=?,
58            behoerdenid=?
59    ';
60
61    const QUERY_DELETE_ASSIGNED_DEPARTMENTS = '
62        DELETE FROM
63            ' . self::TABLE_ASSIGNMENT . '
64        WHERE
65            nutzerid=?
66        ORDER BY behoerdenid
67    ';
68
69    const QUERY_DELETE_USER_ROLES = '
70        DELETE FROM user_role WHERE user_id = ?
71    ';
72
73    const QUERY_INSERT_USER_ROLES_BY_NAME = '
74        INSERT INTO user_role (user_id, role_id)
75        SELECT ?, r.id FROM role r WHERE r.name IN (:roleNames)
76    ';
77
78    const QUERY_READ_SUPERUSER_DEPARTMENTS = '
79        SELECT behoerde.`BehoerdenID` AS id
80        FROM ' . Department::TABLE . '
81        ORDER BY behoerde.Name
82    ';
83
84    const QUERY_READ_ASSIGNED_DEPARTMENTS = '
85        SELECT userAssignment.`behoerdenid` AS id
86        FROM ' . self::TABLE_ASSIGNMENT . ' userAssignment
87        LEFT JOIN ' . self::TABLE . ' useraccount ON useraccount.Name = :useraccountName
88        WHERE
89            useraccount.`NutzerID` = userAssignment.`nutzerid`
90        ORDER BY userAssignment.`behoerdenid`
91    ';
92
93    const QUERY_READ_ASSIGNED_DEPARTMENTS_FOR_ALL = '
94        SELECT useraccount.Name as useraccountName,
95            userAssignment.`behoerdenid` AS id
96        FROM ' . self::TABLE_ASSIGNMENT . ' userAssignment
97        LEFT JOIN ' . self::TABLE . ' useraccount ON useraccount.NutzerID = userAssignment.nutzerid
98        WHERE
99            useraccount.Name IN (:useraccountNames)
100        ORDER BY useraccount.Name, userAssignment.`behoerdenid`
101    ';
102
103    /**
104     * Build an SQL expression that checks whether the current useraccount has
105     * a permission via user_role -> role_permission -> permission.
106     */
107    protected function permissionExists(string $permissionName)
108    {
109        if (!in_array($permissionName, self::VALID_PERMISSION_NAMES, true)) {
110            throw new \InvalidArgumentException("Invalid permission name: $permissionName");
111        }
112        $quoted = "'" . $permissionName . "'";
113        return self::expression(
114            'EXISTS('
115            . 'SELECT 1 '
116            . 'FROM user_role ur '
117            . 'JOIN role_permission rp ON rp.role_id = ur.role_id '
118            . 'JOIN permission p ON p.id = rp.permission_id '
119            . 'WHERE ur.user_id = useraccount.NutzerID '
120            . 'AND p.name = ' . $quoted
121            . ')'
122        );
123    }
124
125    #[\Override]
126    public function getEntityMapping()
127    {
128        return [
129            'id' => 'useraccount.Name',
130            'password' => 'useraccount.Passworthash',
131            'lastLogin' => 'useraccount.lastUpdate',
132            'roles' => self::expression(
133                '(SELECT GROUP_CONCAT(DISTINCT r.name ORDER BY r.name SEPARATOR \',\') '
134                . 'FROM user_role ur '
135                . 'JOIN role r ON r.id = ur.role_id '
136                . 'WHERE ur.user_id = useraccount.NutzerID)'
137            ),
138            'rights__superuser' => self::expression('`useraccount`.`Berechtigung` = 90'),
139            'rights__organisation' => self::expression('`useraccount`.`Berechtigung` >= 70'),
140            'rights__department' => self::expression('`useraccount`.`Berechtigung` >= 50'),
141            'rights__cluster' => self::expression('`useraccount`.`Berechtigung` >= 40'),
142            'rights__useraccount' => self::expression('`useraccount`.`Berechtigung` >= 40'),
143            'rights__scope' => self::expression('`useraccount`.`Berechtigung` >= 30'),
144            'rights__departmentStats' => self::expression('`useraccount`.`Berechtigung` >= 25'),
145            'rights__availability' => self::expression('`useraccount`.`Berechtigung` >= 20'),
146            'rights__ticketprinter' => self::expression('`useraccount`.`Berechtigung` >= 15'),
147            'rights__audit' => self::expression('`useraccount`.`Berechtigung` = 5 OR `useraccount`.`Berechtigung` = 90'),
148            'rights__basic' => self::expression('`useraccount`.`Berechtigung` >= 0'),
149            'permissions__appointment' => $this->permissionExists('appointment'),
150            'permissions__availability' => $this->permissionExists('availability'),
151            'permissions__calldisplay' => $this->permissionExists('calldisplay'),
152            'permissions__cherrypick' => $this->permissionExists('cherrypick'),
153            'permissions__cluster' => $this->permissionExists('cluster'),
154            'permissions__config' => $this->permissionExists('config'),
155            'permissions__counter' => $this->permissionExists('counter'),
156            'permissions__customersearch' => $this->permissionExists('customersearch'),
157            'permissions__dayoff' => $this->permissionExists('dayoff'),
158            'permissions__department' => $this->permissionExists('department'),
159            'permissions__emergency' => $this->permissionExists('emergency'),
160            'permissions__finishedqueue' => $this->permissionExists('finishedqueue'),
161            'permissions__finishedqueuepast' => $this->permissionExists('finishedqueuepast'),
162            'permissions__logs' => $this->permissionExists('logs'),
163            'permissions__mailtemplates' => $this->permissionExists('mailtemplates'),
164            'permissions__missedqueue' => $this->permissionExists('missedqueue'),
165            'permissions__openqueue' => $this->permissionExists('openqueue'),
166            'permissions__organisation' => $this->permissionExists('organisation'),
167            'permissions__overviewcalendar' => $this->permissionExists('overviewcalendar'),
168            'permissions__parkedqueue' => $this->permissionExists('parkedqueue'),
169            'permissions__restrictedscope' => $this->permissionExists('restrictedscope'),
170            'permissions__scope' => $this->permissionExists('scope'),
171            'permissions__source' => $this->permissionExists('source'),
172            'permissions__statistic' => $this->permissionExists('statistic'),
173            'permissions__ticketprinter' => $this->permissionExists('ticketprinter'),
174            'permissions__useraccount' => $this->permissionExists('useraccount'),
175            'permissions__waitingqueue' => $this->permissionExists('waitingqueue'),
176            'permissions__superuser' => $this->permissionExists('superuser'),
177        ];
178    }
179
180    public function addConditionLoginName($loginName)
181    {
182        $this->query->where('useraccount.Name', '=', $loginName);
183        return $this;
184    }
185
186    public function addConditionUserId($userId)
187    {
188        $this->query->where('useraccount.NutzerID', '=', $userId);
189        return $this;
190    }
191
192    public function addConditionPassword($password)
193    {
194        $this->query->where('useraccount.Passworthash', '=', $password);
195        return $this;
196    }
197
198    public function addConditionXauthKey($xAuthKey)
199    {
200        $this->query->where('useraccount.SessionID', '=', $xAuthKey);
201        $this->query->where('useraccount.SessionExpiry', '>', date('Y-m-d H:i:s', time() - App::SESSION_DURATION));
202        return $this;
203    }
204
205    public function addConditionRoleLevel($roleLevel)
206    {
207        $this->query->where('useraccount.Berechtigung', '=', $roleLevel);
208        return $this;
209    }
210
211    public function addConditionSearch($queryString, $orWhere = false)
212    {
213        $condition = function (\BO\Zmsdb\Query\Builder\ConditionBuilder $query) use ($queryString) {
214            $queryString = trim($queryString);
215            $query->orWith('useraccount.NutzerID', 'LIKE', "%$queryString%");
216            $query->orWith('useraccount.Name', 'LIKE', "%$queryString%");
217        };
218        if ($orWhere) {
219            $this->query->orWhere($condition);
220        } else {
221            $this->query->where($condition);
222        }
223        return $this;
224    }
225
226    public function reverseEntityMapping(\BO\Zmsentities\Useraccount $entity)
227    {
228        $data = array();
229        $data['Name'] = $entity->id;
230        $data['Passworthash'] = (isset($entity->password)) ? $entity->password : null;
231        $data['Berechtigung'] = $entity->getRightsLevel();
232        $data['BehoerdenID'] = 0;
233        if (!$entity->isSuperUser() && isset($entity->departments) && 0 < $entity->departments->count()) {
234            $data['BehoerdenID'] = $entity->departments->getFirst()->id;
235        }
236        //default values because of strict mode
237        $data['notrufinitiierung'] = 0;
238        $data['notrufantwort'] = 0;
239
240        $data = array_filter($data, function ($value) {
241            return ($value !== null && $value !== false);
242        });
243        return $data;
244    }
245
246    #[\Override]
247    public function postProcess($data)
248    {
249        $data[$this->getPrefixed("lastLogin")] = ('0000-00-00' != $data[$this->getPrefixed("lastLogin")]) ?
250            strtotime($data[$this->getPrefixed("lastLogin")]) :
251            null;
252
253        $rolesKey = $this->getPrefixed('roles');
254        $rawRoles = $data[$rolesKey] ?? null;
255        if ($rawRoles === null || $rawRoles === '') {
256            $data[$rolesKey] = [];
257        } elseif (is_string($rawRoles)) {
258            $data[$rolesKey] = array_values(array_filter(array_map('trim', explode(',', $rawRoles)), function ($v) {
259                return $v !== '';
260            }));
261        }
262
263        $permissionsPrefix = $this->getPrefixed('permissions__');
264        foreach ($data as $key => $value) {
265            if (0 === strpos($key, $permissionsPrefix)) {
266                $data[$key] = (bool) $value;
267            }
268        }
269
270        return $data;
271    }
272
273    public function addConditionDepartmentIds(array $departmentIds)
274    {
275        $this->setDistinctSelect();
276        $this->innerJoin(
277            new Alias(static::TABLE_ASSIGNMENT, 'useraccount_department'),
278            'useraccount.NutzerID',
279            '=',
280            'useraccount_department.nutzerid'
281        );
282        $this->query->where('useraccount_department.behoerdenid', 'IN', $departmentIds);
283        return $this;
284    }
285
286    public function addConditionDepartmentIdsAndSearch(array $departmentIds, $queryString = null, $orWhere = false): self
287    {
288        $this->addConditionDepartmentIds($departmentIds);
289
290        if ($queryString) {
291            $this->addConditionSearch($queryString, $orWhere);
292        }
293
294        return $this;
295    }
296
297    public function addConditionExcludeSuperusers(): self
298    {
299        $this->query->where('useraccount.Berechtigung', '!=', 90);
300        return $this;
301    }
302
303    public function addOrderByName(): self
304    {
305        $this->query->orderBy('useraccount.Name', 'ASC');
306        return $this;
307    }
308
309    /**
310     * @SuppressWarnings(UnusedFormalParameter)
311     */
312    public function addConditionWorkstationAccess($workstationUserId, array $workstationDepartmentIds, $isWorkstationSuperuser = false): self
313    {
314        // Superusers can access all useraccounts, no filtering needed
315        if ($isWorkstationSuperuser) {
316            return $this;
317        }
318
319        // Always exclude superusers for non-superuser workstation users
320        $this->query->where('useraccount.Berechtigung', '!=', 90);
321
322        // If no departments, only exclude superusers (already done above)
323        if (empty($workstationDepartmentIds)) {
324            return $this;
325        }
326
327        // Ensure we have a join to nutzerzuordnung for target useraccounts
328        $this->setDistinctSelect();
329        $this->innerJoin(
330            new Alias(static::TABLE_ASSIGNMENT, 'useraccount_department'),
331            'useraccount.NutzerID',
332            '=',
333            'useraccount_department.nutzerid'
334        );
335
336        // Target useraccount must share at least one department with workstation user
337        $this->query->where('useraccount_department.behoerdenid', 'IN', $workstationDepartmentIds);
338
339        return $this;
340    }
341}