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