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