Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.06% covered (success)
90.06%
145 / 161
80.77% covered (warning)
80.77%
21 / 26
CRAP
0.00% covered (danger)
0.00%
0 / 1
Useraccount
90.06% covered (success)
90.06%
145 / 161
80.77% covered (warning)
80.77%
21 / 26
75.95
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
 hasRights
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 testRights
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    public function getDefaults()
19    {
20        return [
21            'rights' => [
22                "availability" => false,
23                "basic" => true,
24                "cluster" => false,
25                "department" => false,
26                "organisation" => false,
27                "scope" => false,
28                "superuser" => false,
29                "ticketprinter" => false,
30                "useraccount" => false,
31            ],
32            'permissions' => [
33                "appointment" => false,
34                "availability" => false,
35                "calldisplay" => false,
36                "cherrypick" => false,
37                "cluster" => false,
38                "config" => false,
39                "counter" => false,
40                "customersearch" => false,
41                "dayoff" => false,
42                "department" => false,
43                "emergency" => false,
44                "finishedqueue" => false,
45                "finishedqueuepast" => false,
46                "logs" => false,
47                "mailtemplates" => false,
48                "missedqueue" => false,
49                "openqueue" => false,
50                "organisation" => false,
51                "overviewcalendar" => false,
52                "parkedqueue" => false,
53                "restrictedscope" => false,
54                "scope" => false,
55                "source" => false,
56                "statistic" => false,
57                "ticketprinter" => false,
58                "useraccount" => false,
59                "waitingqueue" => false,
60                "superuser" => false
61            ],
62            'departments' => new Collection\DepartmentList(),
63        ];
64    }
65
66    public function hasProperties()
67    {
68        foreach (func_get_args() as $property) {
69            if (!$this->toProperty()->$property->get()) {
70                throw new Exception\UserAccountMissingProperties("Missing property " . htmlspecialchars($property));
71                return false;
72            }
73        }
74        return true;
75    }
76
77    public function getDepartmentList()
78    {
79        if (!$this->departments instanceof Collection\DepartmentList) {
80            $this->departments = new Collection\DepartmentList($this->departments);
81            foreach ($this->departments as $key => $department) {
82                $this->departments[$key] = new Department($department);
83            }
84        }
85        return $this->departments;
86    }
87
88    public function addDepartment($department)
89    {
90        $this->departments[] = $department;
91        return $this;
92    }
93
94    public function getDepartment($departmentId)
95    {
96        if (count($this->departments)) {
97            foreach ($this->getDepartmentList() as $department) {
98                if ($department['id'] == $departmentId) {
99                    return $department;
100                }
101            }
102        }
103        return new Department(['name' => 'Not existing']);
104    }
105
106    public function hasDepartment($departmentId)
107    {
108        return $this->getDepartment($departmentId)->hasId();
109    }
110
111    public function hasScope($scopeId)
112    {
113        return $this->getDepartmentList()->getUniqueScopeList()->hasEntity($scopeId);
114    }
115
116    /**
117     * @todo Remove this function, keep no contraint on old DB schema in zmsentities
118     */
119    public function getRightsLevel()
120    {
121        return Helper\RightsLevelManager::getLevel($this->rights);
122    }
123
124    public function setRights()
125    {
126        $givenRights = func_get_args();
127        foreach ($givenRights as $right) {
128            if (Property::__keyExists($right, $this->rights)) {
129                $this->rights[$right] = true;
130            }
131        }
132        return $this;
133    }
134
135    // @todo Legacy cleanup â€” remove rights path once migration to permissions is complete.
136    public function hasRights(array $requiredRights): bool
137    {
138        if ($this->isSuperUser()) {
139            return true;
140        }
141
142        $permissions = $this->toProperty()->permissions ?? null;
143        $rights = $this->toProperty()->rights ?? null;
144
145        foreach ($requiredRights as $required) {
146            if ($required instanceof Useraccount\RightsInterface) {
147                if (!$required->validateUseraccount($this)) {
148                    return false;
149                }
150                continue;
151            }
152
153            $hasPermission = $permissions?->$required?->get() ?? false;
154            $hasRight = $rights?->$required?->get() ?? false;
155
156            if (!$hasPermission && !$hasRight) {
157                return false;
158            }
159        }
160        return true;
161    }
162
163    public function testRights(array $requiredRights)
164    {
165        if ($this->hasId()) {
166            if (!$this->hasRights($requiredRights)) {
167                throw new Exception\UserAccountMissingRights(
168                    "Missing rights " . htmlspecialchars(implode(',', $requiredRights))
169                );
170            }
171        } else {
172            throw new Exception\UserAccountMissingLogin();
173        }
174        return $this;
175    }
176
177    public function isOveraged(\DateTimeInterface $dateTime)
178    {
179        if (Property::__keyExists('lastLogin', $this)) {
180            $lastLogin = (new \DateTimeImmutable())->setTimestamp($this['lastLogin'])->modify('23:59:59');
181            return ($lastLogin < $dateTime);
182        }
183        return false;
184    }
185
186    public function isSuperUser(): bool
187    {
188        return $this->toProperty()->rights?->superuser?->get()
189            || $this->toProperty()->permissions?->superuser?->get()
190            ?? false;
191    }
192
193    public function getDepartmentById($departmentId)
194    {
195        foreach ($this->departments as $department) {
196            if ($departmentId == $department['id']) {
197                return new Department($department);
198            }
199        }
200        return new Department();
201    }
202
203    public function getDepartmentByIds(array $departmentIds)
204    {
205        foreach ($this->departments as $department) {
206            if (in_array($department['id'], $departmentIds)) {
207                return new Department($department);
208            }
209        }
210        return new Department();
211    }
212
213    public function testDepartmentById($departmentId)
214    {
215        $department = $this->getDepartmentById($departmentId);
216        if (!$department->hasId()) {
217            throw new Exception\UserAccountMissingDepartment(
218                "Missing department " . htmlspecialchars($departmentId)
219            );
220        }
221        return $department;
222    }
223
224    public function setPassword($input)
225    {
226        if (isset($input['password']) && '' != $input['password']) {
227            $this->password = $input['password'];
228        }
229        if (isset($input['changePassword']) && 0 < count(array_filter($input['changePassword']))) {
230            if (! isset($input['password'])) {
231                $this->password = $input['changePassword'][0];
232            }
233            $this->changePassword = $input['changePassword'];
234        }
235        return $this;
236    }
237
238    public function withDepartmentList()
239    {
240        $departmentList = new Collection\DepartmentList();
241        $entity = clone $this;
242        foreach ($this->departments as $department) {
243            if (! is_array($department) && ! $department instanceof Department) {
244                $department = new Department(array('id' => $department));
245            }
246            $departmentList->addEntity($department);
247        }
248        $entity->departments = $departmentList;
249        return $entity;
250    }
251
252    public function withCleanedUpFormData($keepPassword = false)
253    {
254        unset($this['save']);
255        if (isset($this['password']) && '' == $this['password'] && false === $keepPassword) {
256            unset($this['password']);
257        }
258        if (
259            isset($this['changePassword']) &&
260            0 == count(array_filter($this['changePassword'])) &&
261            false === $keepPassword
262        ) {
263            unset($this['changePassword']);
264        }
265        if (isset($this['oidcProvider'])) {
266            unset($this['oidcProvider']);
267        }
268
269        return $this;
270    }
271
272    /**
273     * verify hashed password and create new if needs rehash
274     *
275     * @return array $useraccount
276    */
277    public function setVerifiedHash($password)
278    {
279        // Do you have old, turbo-legacy, non-crypt hashes?
280        if (strpos($this->password, '$') !== 0) {
281            //error_log(__METHOD__ . "::legacy_hash\n");
282            $result = $this->password === md5($password);
283        } else {
284            //error_log(__METHOD__ . "::password_verify\n");
285            $result = password_verify($password, $this->password);
286        }
287
288        // on passed validation check if the hash needs updating.
289        if ($result && $this->isPasswordNeedingRehash()) {
290            $this->password = $this->getHash($password);
291            //error_log(__METHOD__ . "::rehash\n");
292        }
293
294        return $this;
295    }
296
297    public function withVerifiedHash($password)
298    {
299        $useraccount = clone $this;
300        if ($useraccount->isPasswordNeedingRehash()) {
301            $useraccount->setVerifiedHash($password);
302        }
303        return $useraccount;
304    }
305
306    public function isPasswordNeedingRehash()
307    {
308        return password_needs_rehash($this->password, PASSWORD_DEFAULT);
309    }
310
311    /**
312     * set salted hash by string
313     *
314     * @return string $hash
315    */
316    public function getHash($string)
317    {
318        $hash = password_hash($string, PASSWORD_DEFAULT);
319        return $hash;
320    }
321
322    public function withLessData()
323    {
324        unset($this->departments);
325
326        return $this;
327    }
328
329    /**
330     * create useraccount from open id input data with random password
331     *
332     * @return string $entity
333    */
334    public function createFromOpenidData($data)
335    {
336        $entity = new self();
337        $entity->id = $data['username'];
338        $department = new Department(['id' => 0]);
339        $entity->addDepartment($department);
340        $password = substr(str_shuffle($entity->id . uniqid()), 0, 8);
341        $entity->password = $this->getHash($password);
342        return $entity;
343    }
344
345    /**
346     * get oidc provider from $entity id if it exists
347     *
348     * @return string $entity
349    */
350    public function getOidcProviderFromName()
351    {
352        $providerName = '';
353        if (($pos = strpos($this->id, "@")) !== false) {
354            $providerName = substr($this->id, $pos + 1);
355        }
356        return ('' !== $providerName) ? $providerName : null;
357    }
358}