Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
92.52% covered (success)
92.52%
198 / 214
84.38% covered (warning)
84.38%
27 / 32
CRAP
0.00% covered (danger)
0.00%
0 / 1
Useraccount
92.52% covered (success)
92.52%
198 / 214
84.38% covered (warning)
84.38%
27 / 32
106.35
0.00% covered (danger)
0.00%
0 / 1
 getDefaults
100.00% covered (success)
100.00%
44 / 44
100.00% covered (success)
100.00%
1 / 1
1
 hasProperties
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getDepartmentList
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 addDepartment
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDepartment
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 hasDepartment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasScope
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRightsLevel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRights
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setPermissions
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 hasRights
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 hasPermissions
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 hasAnyPermission
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
6
 hasExclusivePermission
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
10
 testRights
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 testPermissions
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 testAnyPermission
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isOveraged
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isSuperUser
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getDepartmentById
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getDepartmentByIds
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 testDepartmentById
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 setPassword
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
6
 withDepartmentList
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 withCleanedUpFormData
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
8.06
 setVerifiedHash
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 withVerifiedHash
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 isPasswordNeedingRehash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHash
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 withLessData
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createFromOpenidData
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getOidcProviderFromName
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace BO\Zmsentities;
4
5use BO\Zmsentities\Helper\Property;
6
7/**
8 * @SuppressWarnings(Complexity)
9 * @SuppressWarnings(PublicMethod)
10 *
11 */
12class Useraccount extends Schema\Entity
13{
14    public const PRIMARY = 'id';
15
16    public static $schema = "useraccount.json";
17
18    #[\Override]
19    public function getDefaults()
20    {
21        return [
22            'rights' => [
23                "availability" => false,
24                "basic" => true,
25                "cluster" => false,
26                "department" => false,
27                "organisation" => false,
28                "scope" => false,
29                "superuser" => false,
30                "ticketprinter" => false,
31                "useraccount" => false,
32            ],
33            'permissions' => [
34                "appointment" => false,
35                "availability" => false,
36                "calldisplay" => false,
37                "cherrypick" => false,
38                "cluster" => false,
39                "config" => false,
40                "counter" => false,
41                "customersearch" => false,
42                "dayoff" => false,
43                "department" => false,
44                "emergency" => false,
45                "finishedqueue" => false,
46                "finishedqueuepast" => false,
47                "logs" => false,
48                "mailtemplates" => false,
49                "missedqueue" => false,
50                "openqueue" => false,
51                "organisation" => false,
52                "overviewcalendar" => false,
53                "parkedqueue" => false,
54                "restrictedscope" => false,
55                "scope" => false,
56                "source" => false,
57                "statistic" => false,
58                "ticketprinter" => false,
59                "useraccount" => false,
60                "waitingqueue" => false,
61                "superuser" => false
62            ],
63            'departments' => new Collection\DepartmentList(),
64        ];
65    }
66
67    public function hasProperties()
68    {
69        foreach (func_get_args() as $property) {
70            if (!$this->toProperty()->$property->get()) {
71                throw new Exception\UserAccountMissingProperties("Missing property " . htmlspecialchars($property));
72                return false;
73            }
74        }
75        return true;
76    }
77
78    public function getDepartmentList()
79    {
80        if (!$this->departments instanceof Collection\DepartmentList) {
81            $this->departments = new Collection\DepartmentList($this->departments);
82            foreach ($this->departments as $key => $department) {
83                $this->departments[$key] = new Department($department);
84            }
85        }
86        return $this->departments;
87    }
88
89    public function addDepartment($department)
90    {
91        $this->departments[] = $department;
92        return $this;
93    }
94
95    public function getDepartment($departmentId)
96    {
97        if (count($this->departments)) {
98            foreach ($this->getDepartmentList() as $department) {
99                if ($department['id'] == $departmentId) {
100                    return $department;
101                }
102            }
103        }
104        return new Department(['name' => 'Not existing']);
105    }
106
107    public function hasDepartment($departmentId)
108    {
109        return $this->getDepartment($departmentId)->hasId();
110    }
111
112    public function hasScope($scopeId)
113    {
114        return $this->getDepartmentList()->getUniqueScopeList()->hasEntity($scopeId);
115    }
116
117    /**
118     * @todo Remove this function, keep no contraint on old DB schema in zmsentities
119     */
120    public function getRightsLevel()
121    {
122        return Helper\RightsLevelManager::getLevel($this->rights);
123    }
124
125    public function setRights()
126    {
127        $givenRights = func_get_args();
128        foreach ($givenRights as $right) {
129            if (Property::__keyExists($right, $this->rights)) {
130                $this->rights[$right] = true;
131            }
132        }
133        return $this;
134    }
135
136    public function setPermissions()
137    {
138        $givenPermissions = func_get_args();
139        foreach ($givenPermissions as $permission) {
140            if (Property::__keyExists($permission, $this->permissions)) {
141                $this->permissions[$permission] = true;
142            }
143        }
144        return $this;
145    }
146
147    // @todo Legacy cleanup â€” remove rights path once migration to permissions is complete.
148    public function hasRights(array $requiredRights): bool
149    {
150        if ($this->isSuperUser()) {
151            return true;
152        }
153
154        $permissions = $this->toProperty()->permissions ?? null;
155        $rights = $this->toProperty()->rights ?? null;
156
157        foreach ($requiredRights as $required) {
158            if ($required instanceof Useraccount\RightsInterface) {
159                if (!$required->validateUseraccount($this)) {
160                    return false;
161                }
162                continue;
163            }
164
165            $hasPermission = $permissions?->$required?->get() ?? false;
166            $hasRight = $rights?->$required?->get() ?? false;
167
168            if (!$hasPermission && !$hasRight) {
169                return false;
170            }
171        }
172        return true;
173    }
174
175    /**
176     * Returns true when the user has all of the given permissions.
177     */
178    public function hasPermissions(array $requiredPermissions): bool
179    {
180        if ($this->isSuperUser()) {
181            return true;
182        }
183
184        $permissions = $this->toProperty()->permissions ?? null;
185
186        foreach ($requiredPermissions as $required) {
187            if ($required instanceof Useraccount\RightsInterface) {
188                if (! $required->validateUseraccount($this)) {
189                    return false;
190                }
191                continue;
192            }
193
194            if (! ($permissions?->$required?->get() ?? false)) {
195                return false;
196            }
197        }
198
199        return true;
200    }
201
202    /**
203     * Returns true when the user has any of the given permissions.
204     */
205    public function hasAnyPermission(array $requiredPermissions): bool
206    {
207        if ($this->isSuperUser()) {
208            return true;
209        }
210
211        $permissions = $this->toProperty()->permissions ?? null;
212
213        foreach ($requiredPermissions as $required) {
214            if ($required instanceof Useraccount\RightsInterface) {
215                if ($required->validateUseraccount($this)) {
216                    return true;
217                }
218                continue;
219            }
220
221            if ($permissions?->$required?->get() ?? false) {
222                return true;
223            }
224        }
225
226        return false;
227    }
228
229    /**
230     * Returns true when the user has only the given permission and no other permission.
231     */
232    public function hasExclusivePermission(string $permission): bool
233    {
234        if ($this->isSuperUser()) {
235            return false;
236        }
237
238        $permissions = $this['permissions'] ?? [];
239        $requiredPermission = $permissions[$permission] ?? false;
240        if (!is_array($permissions) || !$requiredPermission || '0' === $requiredPermission) {
241            return false;
242        }
243
244        foreach ($permissions as $name => $enabled) {
245            if ($permission === $name || 'superuser' === $name) {
246                continue;
247            }
248            if ($enabled && '0' !== $enabled) {
249                return false;
250            }
251        }
252
253        return true;
254    }
255
256    public function testRights(array $requiredRights)
257    {
258        if ($this->hasId()) {
259            if (!$this->hasRights($requiredRights)) {
260                throw new Exception\UserAccountMissingRights(
261                    "Missing rights " . htmlspecialchars(implode(',', $requiredRights))
262                );
263            }
264        } else {
265            throw new Exception\UserAccountMissingLogin();
266        }
267        return $this;
268    }
269
270    public function testPermissions(array $requiredPermissions)
271    {
272        if (! $this->hasId()) {
273            throw new Exception\UserAccountMissingLogin();
274        }
275
276        if (! $this->hasPermissions($requiredPermissions)) {
277            throw new Exception\UserAccountMissingRights(
278                "Missing permissions " . htmlspecialchars(implode(',', $requiredPermissions))
279            );
280        }
281
282        return $this;
283    }
284
285    public function testAnyPermission(array $requiredPermissions)
286    {
287        if (! $this->hasId()) {
288            throw new Exception\UserAccountMissingLogin();
289        }
290
291        if (! $this->hasAnyPermission($requiredPermissions)) {
292            throw new Exception\UserAccountMissingRights(
293                "Missing any of permissions " . htmlspecialchars(implode(',', $requiredPermissions))
294            );
295        }
296
297        return $this;
298    }
299
300    public function isOveraged(\DateTimeInterface $dateTime)
301    {
302        if (Property::__keyExists('lastLogin', $this)) {
303            $lastLogin = (new \DateTimeImmutable())->setTimestamp($this['lastLogin'])->modify('23:59:59');
304            return ($lastLogin < $dateTime);
305        }
306        return false;
307    }
308
309    public function isSuperUser(): bool
310    {
311        return $this->toProperty()->rights?->superuser?->get()
312            || $this->toProperty()->permissions?->superuser?->get()
313            ?? false;
314    }
315
316    public function getDepartmentById($departmentId)
317    {
318        foreach ($this->departments as $department) {
319            if ($departmentId == $department['id']) {
320                return new Department($department);
321            }
322        }
323        return new Department();
324    }
325
326    public function getDepartmentByIds(array $departmentIds)
327    {
328        foreach ($this->departments as $department) {
329            if (in_array($department['id'], $departmentIds)) {
330                return new Department($department);
331            }
332        }
333        return new Department();
334    }
335
336    public function testDepartmentById($departmentId)
337    {
338        $department = $this->getDepartmentById($departmentId);
339        if (!$department->hasId()) {
340            throw new Exception\UserAccountMissingDepartment(
341                "Missing department " . htmlspecialchars($departmentId)
342            );
343        }
344        return $department;
345    }
346
347    public function setPassword($input)
348    {
349        if (isset($input['password']) && '' != $input['password']) {
350            $this->password = $input['password'];
351        }
352        if (isset($input['changePassword']) && 0 < count(array_filter($input['changePassword']))) {
353            if (! isset($input['password'])) {
354                $this->password = $input['changePassword'][0];
355            }
356            $this->changePassword = $input['changePassword'];
357        }
358        return $this;
359    }
360
361    public function withDepartmentList()
362    {
363        $departmentList = new Collection\DepartmentList();
364        $entity = clone $this;
365        foreach ($this->departments as $department) {
366            if (! is_array($department) && ! $department instanceof Department) {
367                $department = new Department(array('id' => $department));
368            }
369            $departmentList->addEntity($department);
370        }
371        $entity->departments = $departmentList;
372        return $entity;
373    }
374
375    #[\Override]
376    public function withCleanedUpFormData($keepPassword = false)
377    {
378        unset($this['save']);
379        if (isset($this['password']) && '' == $this['password'] && false === $keepPassword) {
380            unset($this['password']);
381        }
382        if (
383            isset($this['changePassword']) &&
384            0 == count(array_filter($this['changePassword'])) &&
385            false === $keepPassword
386        ) {
387            unset($this['changePassword']);
388        }
389        if (isset($this['oidcProvider'])) {
390            unset($this['oidcProvider']);
391        }
392
393        return $this;
394    }
395
396    /**
397     * verify hashed password and create new if needs rehash
398     *
399     * @return array $useraccount
400    */
401    public function setVerifiedHash($password)
402    {
403        // Do you have old, turbo-legacy, non-crypt hashes?
404        if (strpos($this->password, '$') !== 0) {
405            $result = $this->password === md5($password);
406        } else {
407            $result = password_verify($password, $this->password);
408        }
409
410        // on passed validation check if the hash needs updating.
411        if ($result && $this->isPasswordNeedingRehash()) {
412            $this->password = $this->getHash($password);
413        }
414
415        return $this;
416    }
417
418    public function withVerifiedHash($password)
419    {
420        $useraccount = clone $this;
421        if ($useraccount->isPasswordNeedingRehash()) {
422            $useraccount->setVerifiedHash($password);
423        }
424        return $useraccount;
425    }
426
427    public function isPasswordNeedingRehash()
428    {
429        return password_needs_rehash($this->password, PASSWORD_DEFAULT);
430    }
431
432    /**
433     * set salted hash by string
434     *
435     * @return string $hash
436    */
437    public function getHash($string)
438    {
439        $hash = password_hash($string, PASSWORD_DEFAULT);
440        return $hash;
441    }
442
443    #[\Override]
444    public function withLessData()
445    {
446        unset($this->departments);
447
448        return $this;
449    }
450
451    /**
452     * create useraccount from open id input data with random password
453     *
454     * @return string $entity
455    */
456    public function createFromOpenidData($data)
457    {
458        $entity = new self();
459        $entity->id = $data['username'];
460        $department = new Department(['id' => 0]);
461        $entity->addDepartment($department);
462        $password = substr(str_shuffle($entity->id . uniqid()), 0, 8);
463        $entity->password = $this->getHash($password);
464        return $entity;
465    }
466
467    /**
468     * get oidc provider from $entity id if it exists
469     *
470     * @return string $entity
471    */
472    public function getOidcProviderFromName()
473    {
474        $providerName = '';
475        if (($pos = strpos($this->id, "@")) !== false) {
476            $providerName = substr($this->id, $pos + 1);
477        }
478        return ('' !== $providerName) ? $providerName : null;
479    }
480}