Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.24% covered (warning)
77.24%
112 / 145
68.75% covered (warning)
68.75%
11 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
User
77.24% covered (warning)
77.24%
112 / 145
68.75% covered (warning)
68.75%
11 / 16
117.35
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 readWorkstation
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
8
 testWorkstationAssigend
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
72
 testWorkstationAccessRights
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
5.20
 testWorkstationAssignedRights
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 testWorkstationAssignedRoles
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
7.02
 checkRights
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 checkPermissions
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 checkAnyPermission
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 checkDepartments
60.00% covered (warning)
60.00%
21 / 35
0.00% covered (danger)
0.00%
0 / 1
21.22
 checkDepartment
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 hasRights
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 hasXApiKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 testWorkstationIsOveraged
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 testReadDepartmentByOrganisation
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 normalizeDepartmentIds
64.29% covered (warning)
64.29%
9 / 14
0.00% covered (danger)
0.00%
0 / 1
6.14
1<?php
2
3namespace BO\Zmsapi\Helper;
4
5use BO\Slim\Render;
6use BO\Zmsdb\Workstation;
7use BO\Zmsapi\Helper\UserAuth;
8use BO\Zmsentities\Collection\DepartmentList;
9
10/**
11 *
12 * @SuppressWarnings(CouplingBetweenObjects)
13 */
14class User
15{
16    public static $workstation = null;
17    public static $workstationResolved = null;
18
19    public static $assignedWorkstation = null;
20
21    public static $request = null;
22
23    private const SUPERUSER_ONLY_ROLES = [
24        'system_admin',
25        'audit_viewer',
26    ];
27
28    public function __construct($request, $resolveReferences = 0)
29    {
30        static::$request = $request;
31        static::readWorkstation($resolveReferences);
32    }
33
34    public static function readWorkstation($resolveReferences = 0)
35    {
36        $request = (static::$request) ? static::$request : Render::$request;
37        if (! static::$workstation) {
38            $useraccount = UserAuth::getUseraccountByAuthMethod($request);
39            if ($useraccount && $useraccount->hasId()) {
40                static::$workstation = (new Workstation())->readEntity($useraccount->id, $resolveReferences);
41                if ($resolveReferences < 1) {
42                    static::$workstation->useraccount = $useraccount;
43                }
44                static::$workstationResolved = $resolveReferences;
45            } else {
46                static::$workstation = new \BO\Zmsentities\Workstation();
47            }
48        }
49        if ($resolveReferences > static::$workstationResolved && static::$workstation->hasId()) {
50            static::$workstation = (new Workstation())
51                ->readResolvedReferences(static::$workstation, $resolveReferences);
52        }
53        return static::$workstation;
54    }
55
56    /**
57     * @throws \BO\Zmsapi\Exception\Workstation\WorkstationAlreadyAssigned
58     *
59     */
60    public static function testWorkstationAssigend(\BO\Zmsentities\Workstation $entity, $resolveReferences = 0)
61    {
62        if (! static::$assignedWorkstation && $entity->name) {
63            static::$assignedWorkstation = (new Workstation())->readWorkstationByScopeAndName(
64                $entity->scope['id'],
65                $entity->name,
66                $resolveReferences
67            );
68        }
69        if (
70            static::$assignedWorkstation &&
71            static::$assignedWorkstation->id != $entity->id &&
72            static::$assignedWorkstation->name == $entity->name &&
73            static::$assignedWorkstation->scope['id'] == $entity->scope['id'] &&
74            ! static::$assignedWorkstation->getUseraccount()->isOveraged(\App::$now)
75        ) {
76            throw new \BO\Zmsapi\Exception\Workstation\WorkstationAlreadyAssigned();
77        }
78    }
79
80    /**
81     * @throws \BO\Zmsentities\Exception\UserAccountAccessRightsFailed
82     *
83     */
84    public static function testWorkstationAccessRights($useraccount)
85    {
86        if (
87            (
88                ! static::$workstation->getUseraccount()->isSuperUser() &&
89                ! static::$workstation->hasAccessToUseraccount($useraccount)
90            ) ||
91            (
92                ! static::$workstation->getUseraccount()->isSuperUser() &&
93                $useraccount->isSuperUser()
94            )
95        ) {
96            throw new \BO\Zmsentities\Exception\UserAccountAccessRightsFailed();
97        }
98    }
99
100    /**
101     * @throws \BO\Zmsentities\Exception\UserAccountMissingRights
102     * @throws \BO\Zmsentities\Exception\UserAccountMissingLogin
103     *
104     */
105    public static function testWorkstationAssignedRights($useraccount)
106    {
107        static::$workstation
108            ->getUseraccount()
109            ->testRights(
110                array_keys(
111                    array_filter($useraccount->rights, function ($right) {
112                        return (1 == $right);
113                    })
114                )
115            );
116    }
117
118    public static function testWorkstationAssignedRoles($useraccount): void
119    {
120        if (! $useraccount->offsetExists('roles') || ! is_array($useraccount['roles'])) {
121            throw new \BO\Zmsapi\Exception\Useraccount\UseraccountInvalidRoleAssignment();
122        }
123
124        $roleNames = array_values(array_unique(array_filter(
125            array_map(
126                static fn ($roleName) => is_string($roleName) ? trim($roleName) : '',
127                $useraccount['roles']
128            )
129        )));
130
131        if (count($roleNames) !== 1) {
132            throw new \BO\Zmsapi\Exception\Useraccount\UseraccountInvalidRoleAssignment();
133        }
134
135        if (
136            ! static::$workstation->getUseraccount()->isSuperUser()
137            && array_intersect($roleNames, self::SUPERUSER_ONLY_ROLES)
138        ) {
139            throw new \BO\Zmsentities\Exception\UserAccountMissingRights();
140        }
141
142        $useraccount['roles'] = $roleNames;
143    }
144
145    /**
146     * @return \BO\Zmsentities\Workstation
147     *
148     */
149    public static function checkRights(...$requiredRights)
150    {
151        $workstation = static::readWorkstation();
152        if (\App::RIGHTSCHECK_ENABLED) {
153            $workstation->getUseraccount()->testRights($requiredRights);
154        }
155        return $workstation;
156    }
157
158    public static function checkPermissions(...$requiredPermissions)
159    {
160        $workstation = static::readWorkstation();
161
162        if (\App::RIGHTSCHECK_ENABLED) {
163            $workstation->getUseraccount()->testPermissions($requiredPermissions);
164        }
165
166        return $workstation;
167    }
168
169    public static function checkAnyPermission(...$requiredPermissions)
170    {
171        $workstation = static::readWorkstation();
172
173        if (\App::RIGHTSCHECK_ENABLED) {
174            $workstation->getUseraccount()->testAnyPermission($requiredPermissions);
175        }
176
177        return $workstation;
178    }
179
180    public static function checkDepartments($departmentIds)
181    {
182        $normalizedIds = self::normalizeDepartmentIds($departmentIds);
183        $departments = new DepartmentList();
184
185        if (empty($normalizedIds)) {
186            return $departments;
187        }
188
189        $workstation = static::readWorkstation(2);
190        $userAccount = $workstation->getUseraccount();
191
192        if (! $userAccount->hasId()) {
193            throw new \BO\Zmsentities\Exception\UserAccountMissingLogin();
194        }
195
196        if ($userAccount->isSuperUser()) {
197            // Bulk-load all departments in one query for superusers
198            $departmentMap = (new \BO\Zmsdb\Department())->readEntitiesByIds($normalizedIds, 1);
199            foreach ($normalizedIds as $departmentId) {
200                if (!isset($departmentMap[$departmentId])) {
201                    throw new \BO\Zmsentities\Exception\UserAccountMissingDepartment(
202                        "No access to department " . htmlspecialchars((string) $departmentId)
203                    );
204                }
205                $departments->addEntity($departmentMap[$departmentId]);
206            }
207        } elseif ($userAccount->hasRights(['department'])) {
208            // Users with 'department' rights: need organisation-based access checks
209            // Group departments by organisation and load in batches
210            foreach ($normalizedIds as $departmentId) {
211                $departments->addEntity(self::checkDepartment($departmentId));
212            }
213        } else {
214            // Regular users: extract departments directly from already-loaded user department list
215            $userDepartmentList = $userAccount->getDepartmentList();
216            $accessibleDepartmentIds = $userDepartmentList->getIds();
217            $accessibleRequestedIds = array_intersect($normalizedIds, $accessibleDepartmentIds);
218
219            if (count($accessibleRequestedIds) !== count($normalizedIds)) {
220                // Some requested departments are not accessible
221                $missingIds = array_diff($normalizedIds, $accessibleRequestedIds);
222                throw new \BO\Zmsentities\Exception\UserAccountMissingDepartment(
223                    "No access to department(s): " . implode(', ', array_map('htmlspecialchars', $missingIds))
224                );
225            }
226
227            // Extract requested departments from already-loaded list (no DB query needed)
228            foreach ($accessibleRequestedIds as $departmentId) {
229                $department = $userDepartmentList->getEntity($departmentId);
230                if (!$department || !$department->hasId()) {
231                    throw new \BO\Zmsentities\Exception\UserAccountMissingDepartment(
232                        "No access to department " . htmlspecialchars((string) $departmentId)
233                    );
234                }
235                $departments->addEntity($department);
236            }
237        }
238
239        return $departments;
240    }
241
242    /**
243     * @return \BO\Zmsentities\Department
244     *
245     */
246    public static function checkDepartment($departmentId)
247    {
248        $workstation = static::readWorkstation(2);
249        $userAccount = $workstation->getUseraccount();
250        if (! $userAccount->hasId()) {
251            throw new \BO\Zmsentities\Exception\UserAccountMissingLogin();
252        }
253        if ($userAccount->isSuperUser()) {
254            $department = (new \BO\Zmsdb\Department())->readEntity($departmentId);
255        } elseif ($userAccount->hasRights(['department'])) {
256            $department = self::testReadDepartmentByOrganisation($departmentId, $userAccount);
257        } else {
258            $department = $userAccount->testDepartmentById($departmentId);
259        }
260        if (! $department) {
261            throw new \BO\Zmsentities\Exception\UserAccountMissingDepartment(
262                "No access to department " . htmlspecialchars($departmentId)
263            );
264        }
265        return $department;
266    }
267
268    public static function hasRights()
269    {
270        $userAccount = static::readWorkstation()->getUseraccount();
271        return $userAccount->hasId();
272    }
273
274    /**
275     * Get X-Api-Key from header
276     *
277    */
278    public static function hasXApiKey($request)
279    {
280        $xApiKeyEntity = null;
281        $xApiKey = $request->getHeaderLine('x-api-key');
282        if ($xApiKey) {
283            $xApiKeyEntity = (new \BO\Zmsdb\Apikey())->readEntity($xApiKey);
284        }
285        return ($xApiKeyEntity && $xApiKeyEntity->hasId());
286    }
287
288    public static function testWorkstationIsOveraged($workstation)
289    {
290        if ($workstation->hasId() && $workstation->getUseraccount()->isOveraged(\App::$now)) {
291            $exception = new \BO\Zmsapi\Exception\Useraccount\AuthKeyFound();
292            $exception->data = $workstation;
293            throw $exception;
294        }
295    }
296
297    protected static function testReadDepartmentByOrganisation($departmentId, $userAccount)
298    {
299        $organisation = (new \BO\Zmsdb\Organisation())->readByDepartmentId($departmentId, 1);
300        $organisation->departments = $organisation->getDepartmentList()->withAccess($userAccount);
301        $department = $organisation->departments->getEntity($departmentId);
302        return $department;
303    }
304
305    public static function normalizeDepartmentIds(array $departmentIds)
306    {
307        $normalized = [];
308        foreach ($departmentIds as $departmentId) {
309            if ($departmentId === null) {
310                continue;
311            }
312            $departmentId = trim((string) $departmentId);
313            if ($departmentId === '') {
314                continue;
315            }
316            $validatedId = filter_var($departmentId, FILTER_VALIDATE_INT);
317            if ($validatedId === false) {
318                throw new \BO\Zmsapi\Exception\BadRequest(
319                    "Invalid department ID: " . htmlspecialchars($departmentId)
320                );
321            }
322            $normalized[] = $validatedId;
323        }
324
325        return array_values(array_unique($normalized));
326    }
327}