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