Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
72.40% |
299 / 413 |
|
38.89% |
7 / 18 |
CRAP | |
0.00% |
0 / 1 |
SessionDAO | |
72.40% |
299 / 413 |
|
38.89% |
7 / 18 |
125.33 | |
0.00% |
0 / 1 |
getToken | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
4 | |||
getOrCreateLoginSession | n/a |
0 / 0 |
n/a |
0 / 0 |
2 | |||||
getLogin | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
4 | |||
createLoginSession | |
96.15% |
25 / 26 |
|
0.00% |
0 / 1 |
3 | |||
getLoginSessionByToken | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
2 | |||
createOrUpdatePersonSession | |
88.00% |
66 / 75 |
|
0.00% |
0 / 1 |
17.50 | |||
getPersonSessionByToken | |
100.00% |
40 / 40 |
|
100.00% |
1 / 1 |
2 | |||
getOrCreateGroupToken | |
90.91% |
20 / 22 |
|
0.00% |
0 / 1 |
3.01 | |||
groupTokenExists | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
getTestStatus | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
2 | |||
personHasBooklet | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
ownsTest | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
getTestsOfPerson | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
6 | |||
deletePersonToken | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupMonitors | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getGroups | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
getDependantSessions | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
getLoginSessions | |
100.00% |
33 / 33 |
|
100.00% |
1 / 1 |
3 | |||
getSysChecksOfPerson | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** @noinspection PhpUnhandledExceptionInspection */ |
3 | declare(strict_types=1); |
4 | |
5 | class SessionDAO extends DAO { |
6 | public function getToken(string $tokenString, array $requiredTypes): AuthToken { |
7 | $tokenInfo = $this->_( |
8 | 'select |
9 | admin_sessions.token, |
10 | users.id, |
11 | \'admin\' as "type", |
12 | -1 as "workspaceId", |
13 | case when (users.is_superadmin) then \'super-admin\' else \'admin\' end as "mode", |
14 | valid_until as "validTo", |
15 | \'[admins]\' as "group" |
16 | from admin_sessions |
17 | left join users on (users.id = admin_sessions.user_id) |
18 | where |
19 | admin_sessions.token = :token |
20 | union |
21 | select |
22 | person_sessions.token, |
23 | person_sessions.id as "id", |
24 | \'person\' as "type", |
25 | logins.workspace_id as "workspaceId", |
26 | logins.mode, |
27 | person_sessions.valid_until as "validTo", |
28 | logins.group_name as "group" |
29 | from person_sessions |
30 | left join login_sessions on (person_sessions.login_sessions_id = login_sessions.id) |
31 | left join logins on (logins.name = login_sessions.name) |
32 | where |
33 | person_sessions.token = :token |
34 | union |
35 | select |
36 | token, |
37 | login_sessions.id as "id", |
38 | \'login\' as "type", |
39 | logins.workspace_id as "workspaceId", |
40 | logins.mode, |
41 | logins.valid_to as "validTo", |
42 | logins.group_name as "group" |
43 | from login_sessions |
44 | left join logins on (logins.name = login_sessions.name) |
45 | where |
46 | login_sessions.token = :token |
47 | limit 1', |
48 | [':token' => $tokenString] |
49 | ); |
50 | |
51 | if ($tokenInfo == null) { |
52 | throw new HttpError("Invalid token: `$tokenString`", 403); |
53 | } |
54 | |
55 | if ($tokenInfo['workspaceId'] == null) { |
56 | throw new HttpError("Login removed: `$tokenString`", 410); |
57 | } |
58 | |
59 | if (!in_array($tokenInfo["type"], $requiredTypes)) { |
60 | throw new HttpError("Token `$tokenString` of " |
61 | . "type `{$tokenInfo["type"]}` has wrong type - `" |
62 | . implode("` or `", $requiredTypes) . "` required.", 403); |
63 | } |
64 | |
65 | TimeStamp::checkExpiration(0, TimeStamp::fromSQLFormat($tokenInfo['validTo'])); |
66 | |
67 | return new AuthToken( |
68 | $tokenInfo['token'], |
69 | (int) $tokenInfo['id'], |
70 | $tokenInfo['type'], |
71 | (int) $tokenInfo['workspaceId'], |
72 | $tokenInfo['mode'], |
73 | $tokenInfo['group'] |
74 | ); |
75 | } |
76 | |
77 | /** |
78 | * @codeCoverageIgnore |
79 | */ |
80 | public function getOrCreateLoginSession(string $name, string $password): LoginSession | FailedLogin { |
81 | $login = $this->getLogin($name, $password); |
82 | |
83 | if (!is_a($login, Login::class)) { |
84 | return $login; |
85 | } |
86 | |
87 | return $this->createLoginSession($login); |
88 | } |
89 | |
90 | public function getLogin(string $name, string $password): Login | FailedLogin { |
91 | $result = $this->_( |
92 | 'select |
93 | logins.name, |
94 | logins.mode, |
95 | logins.group_name, |
96 | logins.group_label, |
97 | login_session_groups.token as group_token, |
98 | logins.codes_to_booklets, |
99 | logins.workspace_id, |
100 | logins.valid_to, |
101 | logins.valid_from, |
102 | logins.valid_for, |
103 | logins.custom_texts, |
104 | logins.password, |
105 | logins.monitors |
106 | from |
107 | logins |
108 | left join login_sessions on (logins.name = login_sessions.name) |
109 | left join login_session_groups on (login_sessions.group_name = login_session_groups.group_name and login_sessions.workspace_id = login_session_groups.workspace_id) |
110 | where |
111 | logins.name = :name', |
112 | [ |
113 | ':name' => $name |
114 | ] |
115 | ); |
116 | |
117 | if (!$result) { |
118 | // we always check one password to not leak the existence of username to time-attacks |
119 | Password::verify($password, 'dummy', 't'); |
120 | return FailedLogin::usernameNotFound; |
121 | } |
122 | |
123 | TimeStamp::checkExpiration( |
124 | TimeStamp::fromSQLFormat($result['valid_from']), |
125 | TimeStamp::fromSQLFormat($result['valid_to']) |
126 | ); |
127 | |
128 | $login = new Login( |
129 | $result['name'], |
130 | '', |
131 | $result['mode'], |
132 | $result['group_name'], |
133 | $result['group_label'], |
134 | JSON::decode($result['codes_to_booklets'], true), |
135 | (int) $result['workspace_id'], |
136 | TimeStamp::fromSQLFormat($result['valid_to']), |
137 | TimeStamp::fromSQLFormat($result['valid_from']), |
138 | (int) $result['valid_for'], |
139 | JSON::decode($result['custom_texts']), |
140 | JSON::decode($result['monitors'], true) |
141 | ); |
142 | |
143 | |
144 | // TODO also use customizable use salt for testees? -> change would break current sessions |
145 | if (!Password::verify($password, $result['password'], 't')) { |
146 | return Mode::hasCapability($login->getMode(), 'protectedLogin') ? |
147 | FailedLogin::wrongPasswordProtectedLogin : |
148 | FailedLogin::wrongPassword; |
149 | } |
150 | |
151 | return $login; |
152 | } |
153 | |
154 | public function createLoginSession(Login $login): LoginSession { |
155 | $loginToken = Token::generate('login', $login->getName()); |
156 | |
157 | // We don't check for existence of the sessions before inserting it because timing issues occurred: If the same |
158 | // login was requested two times at the same moment it could happen that it was created twice. |
159 | |
160 | $this->_( |
161 | 'insert ignore into login_sessions (token, name, workspace_id, group_name) |
162 | values(:token, :name, :ws, :group_name)', |
163 | [ |
164 | ':token' => $loginToken, |
165 | ':name' => $login->getName(), |
166 | ':ws' => $login->getWorkspaceId(), |
167 | ':group_name' => $login->getGroupName() |
168 | ] |
169 | ); |
170 | |
171 | if ($this->lastAffectedRows) { |
172 | $id = (int) $this->pdoDBhandle->lastInsertId(); |
173 | $groupToken = $this->getOrCreateGroupToken($login); |
174 | return new LoginSession($id, $loginToken, $groupToken, $login); |
175 | } |
176 | |
177 | // there is no way in mySQL to combine insert & select into one query, so have to retrieve it to get the id |
178 | $session = $this->_( |
179 | 'select id, token from login_sessions where name = :name and workspace_id = :ws_id', |
180 | [ |
181 | ':name' => $login->getName(), |
182 | ':ws_id' => $login->getWorkspaceId() |
183 | ] |
184 | ); |
185 | |
186 | // usually the musst be a session, because it was just inserted. But in some case of some error conditions: |
187 | if (!$session) { |
188 | throw new Exception("Could not retrieve login-session: `{$login->getName()}`!"); |
189 | } |
190 | |
191 | $groupToken = $this->getOrCreateGroupToken($login); |
192 | return new LoginSession((int) $session['id'], $session['token'], $groupToken, $login); |
193 | } |
194 | |
195 | public function getLoginSessionByToken(string $loginToken): LoginSession { |
196 | $loginSession = $this->_( |
197 | 'select |
198 | login_sessions.id, |
199 | logins.name, |
200 | login_sessions.token, |
201 | logins.mode, |
202 | logins.group_name, |
203 | logins.group_label, |
204 | login_session_groups.token as group_token, |
205 | logins.codes_to_booklets, |
206 | login_sessions.workspace_id, |
207 | logins.custom_texts, |
208 | logins.password, |
209 | logins.valid_for, |
210 | logins.valid_to, |
211 | logins.valid_from, |
212 | logins.monitors |
213 | from |
214 | logins |
215 | left join login_sessions on (logins.name = login_sessions.name) |
216 | left join login_session_groups on (login_sessions.group_name = login_session_groups.group_name and login_sessions.workspace_id = login_session_groups.workspace_id) |
217 | where |
218 | login_sessions.token=:token', |
219 | [':token' => $loginToken] |
220 | ); |
221 | |
222 | if ($loginSession == null) { |
223 | throw new HttpError("LoginToken invalid: `$loginToken`", 403); |
224 | } |
225 | |
226 | TimeStamp::checkExpiration( |
227 | TimeStamp::fromSQLFormat($loginSession['valid_from']), |
228 | TimeStamp::fromSQLFormat($loginSession['valid_to']) |
229 | ); |
230 | |
231 | return new LoginSession( |
232 | (int) $loginSession["id"], |
233 | $loginSession["token"], |
234 | $loginSession["group_token"], |
235 | new Login( |
236 | $loginSession['name'], |
237 | '', |
238 | $loginSession['mode'], |
239 | $loginSession['group_name'], |
240 | $loginSession['group_label'], |
241 | JSON::decode($loginSession['codes_to_booklets'], true), |
242 | (int) $loginSession['workspace_id'], |
243 | TimeStamp::fromSQLFormat($loginSession['valid_to']), |
244 | TimeStamp::fromSQLFormat($loginSession['valid_from']), |
245 | (int) $loginSession['valid_for'], |
246 | JSON::decode($loginSession['custom_texts']), |
247 | JSON::decode($loginSession['monitors'], true) |
248 | ) |
249 | ); |
250 | } |
251 | |
252 | public function createOrUpdatePersonSession( |
253 | LoginSession $loginSession, |
254 | string $code, |
255 | bool $allowExpired = false, |
256 | bool $forceUpdateToken = true |
257 | ): PersonSession { |
258 | $login = $loginSession->getLogin(); |
259 | |
260 | if (count($login->getBooklets()) and !$login->codeExists($code)) { |
261 | throw new HttpError("`$code` is no valid code for `{$login->getName()}`", 400); |
262 | } |
263 | |
264 | if (!$allowExpired) { |
265 | TimeStamp::checkExpiration($login->getValidFrom(), $login->getValidTo()); |
266 | } |
267 | |
268 | $suffix = []; |
269 | if ($code) { |
270 | $suffix[] = $code; |
271 | } |
272 | if (Mode::hasCapability($loginSession->getLogin()->getMode(), 'alwaysNewSession')) { |
273 | // we use random strings to identify the persons, not subsequent numbers, because that caused trouble when |
274 | // two logged in in the very same moment |
275 | $suffix[] = Random::string(8, false); |
276 | } |
277 | $suffix = implode('/', $suffix); |
278 | |
279 | if (!Mode::hasCapability($loginSession->getLogin()->getMode(), 'alwaysNewSession')) { |
280 | $personSession = $this->_(' |
281 | select id, valid_until, token from person_sessions where login_sessions_id = :lsi and name_suffix = :suffix', |
282 | [ |
283 | ':lsi' => $loginSession->getId(), |
284 | ':suffix' => $suffix |
285 | ] |
286 | ); |
287 | |
288 | if ($personSession) { |
289 | if (!$allowExpired) { |
290 | TimeStamp::checkExpiration(0, TimeStamp::fromSQLFormat($personSession['valid_until'])); |
291 | } |
292 | $token = $personSession['token']; |
293 | if (!$token or $forceUpdateToken) { |
294 | $token = Token::generate('person', "{$login->getGroupName()}_{$login->getName()}_$code"); |
295 | $this->_( |
296 | 'update person_sessions set token=:token where login_sessions_id = :lsi and name_suffix = :suffix', |
297 | [ |
298 | ':lsi' => $loginSession->getId(), |
299 | ':suffix' => $suffix, |
300 | ':token' => $token |
301 | ] |
302 | ); |
303 | } |
304 | return new PersonSession( |
305 | $loginSession, |
306 | new Person( |
307 | $personSession['id'], |
308 | $token, |
309 | $code, |
310 | $suffix, |
311 | TimeStamp::fromSQLFormat($personSession['valid_until']) |
312 | ) |
313 | ); |
314 | } |
315 | } |
316 | |
317 | $validUntil = TimeStamp::expirationFromNow($login->getValidTo(), $login->getValidForMinutes()); |
318 | $token = Token::generate('person', "{$login->getGroupName()}_{$login->getName()}_$code"); |
319 | |
320 | try { |
321 | $this->_( |
322 | "insert into person_sessions (token, code, login_sessions_id, valid_until, name_suffix) |
323 | values (:token, :code, :login_id, :valid_until, :suffix)", |
324 | [ |
325 | ':token' => $token, |
326 | ':code' => $code, |
327 | ':login_id' => $loginSession->getId(), |
328 | ':valid_until' => TimeStamp::toSQLFormat($validUntil), |
329 | ':suffix' => $suffix |
330 | ] |
331 | ); |
332 | } catch (Exception $ee) { |
333 | // allow retry on duplicate suffix - unlikely in prod, but always happens in testing when rand is static |
334 | if ($originalException = $ee->getPrevious()) { |
335 | if ( |
336 | property_exists($originalException, 'errorInfo') |
337 | and ($originalException->errorInfo[1] == 1062) |
338 | and ($originalException->getCode() == 23000) |
339 | and (str_ends_with($originalException->errorInfo[2], "for key 'person_sessions.unique_person_session'")) |
340 | ) { |
341 | error_log("Create person-session: retry on duplicate suffix (`{$loginSession->getLogin()->getName()}` / `$suffix`)"); |
342 | return $this->createOrUpdatePersonSession($loginSession, $code, $allowExpired, $forceUpdateToken); |
343 | } |
344 | } |
345 | throw $ee; |
346 | } |
347 | |
348 | |
349 | return new PersonSession( |
350 | $loginSession, |
351 | new Person( |
352 | (int) $this->pdoDBhandle->lastInsertId(), |
353 | $token, |
354 | $code, |
355 | $suffix, |
356 | $validUntil |
357 | ) |
358 | ); |
359 | } |
360 | |
361 | public function getPersonSessionByToken(string $personToken): PersonSession { |
362 | $personSession = $this->_( |
363 | 'select |
364 | login_sessions.id, |
365 | logins.codes_to_booklets, |
366 | login_sessions.workspace_id, |
367 | logins.mode, |
368 | logins.password, |
369 | logins.group_name, |
370 | login_session_groups.group_label, |
371 | login_session_groups.token as group_token, |
372 | login_sessions.token, |
373 | login_sessions.name, |
374 | logins.custom_texts, |
375 | logins.valid_to, |
376 | logins.valid_from, |
377 | logins.valid_for, |
378 | logins.monitors, |
379 | person_sessions.id as "person_id", |
380 | person_sessions.code, |
381 | person_sessions.valid_until, |
382 | person_sessions.name_suffix |
383 | from person_sessions |
384 | inner join login_sessions on login_sessions.id = person_sessions.login_sessions_id |
385 | inner join logins on logins.name = login_sessions.name |
386 | left join login_session_groups on (login_sessions.group_name = login_session_groups.group_name and login_sessions.workspace_id = login_session_groups.workspace_id) |
387 | where person_sessions.token = :token', |
388 | [':token' => $personToken] |
389 | ); |
390 | |
391 | if ($personSession === null) { |
392 | throw new HttpError("PersonToken invalid: `$personToken`", 403); |
393 | } |
394 | |
395 | TimeStamp::checkExpiration(0, Timestamp::fromSQLFormat($personSession['valid_until'])); |
396 | TimeStamp::checkExpiration( |
397 | TimeStamp::fromSQLFormat($personSession['valid_from']), |
398 | TimeStamp::fromSQLFormat($personSession['valid_to']) |
399 | ); |
400 | |
401 | return new PersonSession( |
402 | new LoginSession( |
403 | (int) $personSession['id'], |
404 | $personSession['token'], |
405 | $personSession['group_token'], |
406 | new Login( |
407 | $personSession['name'], |
408 | '', |
409 | $personSession['mode'], |
410 | $personSession['group_name'], |
411 | $personSession['group_label'], |
412 | JSON::decode($personSession['codes_to_booklets'], true), |
413 | (int) $personSession['workspace_id'], |
414 | Timestamp::fromSQLFormat($personSession['valid_to']), |
415 | Timestamp::fromSQLFormat($personSession['valid_from']), |
416 | $personSession['valid_for'], |
417 | JSON::decode($personSession['custom_texts']), |
418 | JSON::decode($personSession['monitors'], true) |
419 | ) |
420 | ), |
421 | new Person( |
422 | (int) $personSession['person_id'], |
423 | $personToken, |
424 | $personSession['code'] ?? '', |
425 | $personSession['name_suffix'] ?? '', |
426 | TimeStamp::fromSQLFormat($personSession['valid_until']) |
427 | ) |
428 | ); |
429 | } |
430 | |
431 | public function getOrCreateGroupToken(Login $login): string { |
432 | $newGroupToken = Token::generate('group', $login->getGroupName()); |
433 | $this->_( |
434 | 'insert ignore into login_session_groups (group_name, workspace_id, group_label, token) values (?, ?, ?, ?)', |
435 | [ |
436 | $login->getGroupName(), |
437 | $login->getWorkspaceId(), |
438 | $login->getGroupLabel(), |
439 | $newGroupToken |
440 | ] |
441 | ); |
442 | |
443 | if ($this->lastAffectedRows) { |
444 | return $newGroupToken; |
445 | } |
446 | |
447 | $res = $this->_( |
448 | 'select token from login_session_groups where group_name = ? and workspace_id = ?', |
449 | [ |
450 | $login->getGroupName(), |
451 | $login->getWorkspaceId() |
452 | ] |
453 | ); |
454 | |
455 | if (!isset($res['token'])) { |
456 | throw new Exception("Could not retrieve group token for `{$login->getGroupName()}`."); |
457 | } |
458 | |
459 | return $res['token']; |
460 | } |
461 | |
462 | |
463 | public function groupTokenExists(int $workspaceId, string $groupTokenString): bool { |
464 | $res = $this->_( |
465 | 'select |
466 | count(token) as count |
467 | from |
468 | login_session_groups |
469 | left join logins on login_session_groups.group_name = logins.group_name |
470 | where |
471 | token = ? and login_session_groups.workspace_id = ?', |
472 | [ |
473 | $groupTokenString, |
474 | $workspaceId |
475 | ] |
476 | ); |
477 | return !!$res['count']; |
478 | } |
479 | |
480 | public function getTestStatus(string $personToken, string $bookletName): array { |
481 | $testStatus = $this->_( |
482 | 'select |
483 | tests.locked, |
484 | tests.running, |
485 | files.label |
486 | from |
487 | person_sessions |
488 | left join login_sessions on (person_sessions.login_sessions_id = login_sessions.id) |
489 | left join logins on (logins.name = login_sessions.name) |
490 | left join files on (files.workspace_id = logins.workspace_id) |
491 | left join tests on (person_sessions.id = tests.person_id and tests.name = files.id) |
492 | where person_sessions.token = :token |
493 | and files.id = :bookletname', |
494 | [ |
495 | ':token' => $personToken, |
496 | ':bookletname' => $bookletName |
497 | ] |
498 | ); |
499 | |
500 | if ($testStatus == null) { |
501 | throw new HttpError("Test `$bookletName` not found!", 404); |
502 | } |
503 | |
504 | $testStatus['running'] = (bool) $testStatus['running']; |
505 | $testStatus['locked'] = (bool) $testStatus['locked']; |
506 | |
507 | return $testStatus; |
508 | } |
509 | |
510 | public function personHasBooklet(string $personToken, string $bookletName): bool { |
511 | $bookletDef = $this->_(' |
512 | select |
513 | logins.codes_to_booklets, |
514 | login_sessions.id, |
515 | person_sessions.code |
516 | from logins |
517 | left join login_sessions on logins.name = login_sessions.name |
518 | left join person_sessions on login_sessions.id = person_sessions.login_sessions_id |
519 | where |
520 | person_sessions.token = :token', |
521 | [ |
522 | ':token' => $personToken |
523 | ] |
524 | ); |
525 | |
526 | $code = $bookletDef['code']; |
527 | $codes2booklets = JSON::decode($bookletDef['codes_to_booklets'], true); |
528 | |
529 | return $codes2booklets and isset($codes2booklets[$code]) and in_array($bookletName, $codes2booklets[$code]); |
530 | } |
531 | |
532 | public function ownsTest(string $personToken, string $testId): bool { |
533 | $test = $this->_( |
534 | 'select tests.locked from tests |
535 | inner join person_sessions on person_sessions.id = tests.person_id |
536 | where person_sessions.token=:token and tests.id=:testId', |
537 | [ |
538 | ':token' => $personToken, |
539 | ':testId' => $testId |
540 | ] |
541 | ); |
542 | |
543 | return !!$test; |
544 | } |
545 | |
546 | public function getTestsOfPerson(PersonSession $personSession): array { |
547 | $bookletIds = $personSession->getLoginSession()->getLogin()->getBooklets()[$personSession->getPerson()->getCode() ?? '']; |
548 | if (!count($bookletIds)) { |
549 | return []; |
550 | } |
551 | $placeHolder = implode(', ', array_fill(0, count($bookletIds), '?')); |
552 | $sql = "select |
553 | tests.person_id, |
554 | tests.id, |
555 | tests.locked, |
556 | tests.running, |
557 | files.name, |
558 | files.id as bookletId, |
559 | files.label as testLabel, |
560 | files.description |
561 | from files |
562 | left outer join tests on files.id = tests.name and tests.person_id = ? |
563 | where |
564 | files.workspace_id = ? |
565 | and files.type = 'Booklet' |
566 | and files.id in ($placeHolder) |
567 | order by |
568 | field(files.id, $placeHolder)"; |
569 | $tests = $this->_( |
570 | $sql, |
571 | [ |
572 | $personSession->getPerson()->getId(), |
573 | $personSession->getLoginSession()->getLogin()->getWorkspaceId(), |
574 | ...$bookletIds, |
575 | ...$bookletIds |
576 | ], |
577 | true |
578 | ); |
579 | return array_map( |
580 | function(array $res): TestData { |
581 | return new TestData( |
582 | (int) $res['id'], |
583 | $res['bookletId'], |
584 | $res['testLabel'], |
585 | $res['description'], |
586 | (bool) $res['locked'], |
587 | (bool) $res['running'], |
588 | (object) [] |
589 | ); |
590 | }, |
591 | $tests |
592 | ); |
593 | } |
594 | |
595 | public function deletePersonToken(AuthToken $authToken): void { |
596 | // we can not delete the session entirely, because this would delete the whole response data. |
597 | $this->_("update person_sessions set token=null where token = :token", [':token' => $authToken->getToken()]); |
598 | } |
599 | |
600 | /** |
601 | * @return Group[] |
602 | */ |
603 | public function getGroupMonitors(PersonSession $personSession): array { |
604 | switch ($personSession->getLoginSession()->getLogin()->getMode()) { |
605 | default: return []; |
606 | case 'monitor-group': |
607 | return [ |
608 | new Group( |
609 | $personSession->getLoginSession()->getLogin()->getGroupName(), |
610 | $personSession->getLoginSession()->getLogin()->getGroupLabel() |
611 | ) |
612 | ]; |
613 | case 'monitor-study': |
614 | return $this->getGroups($personSession->getLoginSession()->getLogin()->getWorkspaceId()); |
615 | } |
616 | } |
617 | |
618 | /** |
619 | * @return Group[] |
620 | */ |
621 | public function getGroups(int $workspaceId): array { |
622 | $modeSelector = "mode in ('" . implode("', '", Mode::getByCapability('monitorable')) . "')"; |
623 | $sql = |
624 | "select |
625 | group_name, |
626 | group_label, |
627 | valid_from, |
628 | valid_to |
629 | from |
630 | logins |
631 | where |
632 | workspace_id = :ws_id |
633 | and $modeSelector |
634 | group by group_name, group_label, valid_from, valid_to |
635 | order by group_label"; |
636 | |
637 | return array_reduce( |
638 | $this->_($sql, [':ws_id' => $workspaceId], true), |
639 | function(array $agg, array $row): array { |
640 | $expiration = TimeStamp::isExpired( |
641 | TimeStamp::fromSQLFormat($row['valid_from']), |
642 | TimeStamp::fromSQLFormat($row['valid_to']) |
643 | ); |
644 | $agg[$row['group_name']] = new Group($row['group_name'], $row['group_label'], $expiration); |
645 | return $agg; |
646 | }, |
647 | [] |
648 | ); |
649 | } |
650 | |
651 | public function getDependantSessions(LoginSession $login): array { |
652 | return match ($login->getLogin()->getMode()) { |
653 | 'monitor-group' => $this->getLoginSessions([ |
654 | 'logins.workspace_id' => $login->getLogin()->getWorkspaceId(), |
655 | 'logins.group_name' => $login->getLogin()->getGroupName() |
656 | ]), |
657 | 'monitor-study' => $this->getLoginSessions([ |
658 | 'logins.workspace_id' => $login->getLogin()->getWorkspaceId() |
659 | ]), |
660 | default => [], |
661 | }; |
662 | } |
663 | |
664 | protected function getLoginSessions(array $filters = []): array { |
665 | $logins = []; |
666 | |
667 | $replacements = []; |
668 | $filterSQL = []; |
669 | foreach ($filters as $filter => $filterValue) { |
670 | $filterName = ':' . str_replace('.', '_', $filter); |
671 | $replacements[$filterName] = $filterValue; |
672 | $filterSQL[] = "$filter = $filterName"; |
673 | } |
674 | $filterSQL = implode(' and ', $filterSQL); |
675 | |
676 | $sql = "select |
677 | logins.name, |
678 | logins.mode, |
679 | logins.group_name, |
680 | logins.group_label, |
681 | logins.codes_to_booklets, |
682 | logins.custom_texts, |
683 | logins.password, |
684 | logins.valid_for, |
685 | logins.valid_to, |
686 | logins.valid_from, |
687 | logins.workspace_id, |
688 | login_sessions.id, |
689 | login_sessions.token, |
690 | login_session_groups.token as group_token |
691 | from |
692 | logins |
693 | left join login_sessions on (logins.name = login_sessions.name) |
694 | left join login_session_groups on (login_sessions.group_name = login_session_groups.group_name and login_sessions.workspace_id = login_session_groups.workspace_id) |
695 | where |
696 | $filterSQL |
697 | order by id"; |
698 | |
699 | $result = $this->_($sql, $replacements, true); |
700 | |
701 | foreach ($result as $row) { |
702 | $logins[] = |
703 | new LoginSession( |
704 | (int) $row["id"], |
705 | $row["token"], |
706 | $row["group_token"], |
707 | new Login( |
708 | $row['name'], |
709 | '', |
710 | $row['mode'], |
711 | $row['group_name'], |
712 | $row['group_label'], |
713 | JSON::decode($row['codes_to_booklets'], true), |
714 | (int) $row['workspace_id'], |
715 | TimeStamp::fromSQLFormat($row['valid_to']), |
716 | TimeStamp::fromSQLFormat($row['valid_from']), |
717 | (int) $row['valid_for'], |
718 | JSON::decode($row['custom_texts']) |
719 | ) |
720 | ); |
721 | } |
722 | |
723 | return $logins; |
724 | } |
725 | |
726 | /** @return SystemCheck[] */ |
727 | public function getSysChecksOfPerson(PersonSession $personSession): array |
728 | { |
729 | $wsId = $personSession->getLoginSession()->getLogin()->getWorkspaceId(); |
730 | $sessionName = $personSession->getLoginSession()->getLogin()->getName(); |
731 | |
732 | $syschecks = $this->_(" |
733 | select * |
734 | from files |
735 | left join logins on files.workspace_id = logins.workspace_id |
736 | where |
737 | files.type = 'SysCheck' and |
738 | logins.name = :session_name and |
739 | logins.workspace_id = :ws_id and |
740 | logins.mode = 'sys-check-login' |
741 | ", |
742 | [ |
743 | 'session_name' => $sessionName, |
744 | 'ws_id' => $wsId, |
745 | ], |
746 | true |
747 | ); |
748 | |
749 | return array_map( |
750 | function (array $sysCheck) { |
751 | return new SystemCheck( |
752 | (string) $sysCheck['workspace_id'], |
753 | (string) $sysCheck['id'], |
754 | $sysCheck['name'], |
755 | $sysCheck['label'], |
756 | $sysCheck['description'] |
757 | ); |
758 | }, |
759 | $syschecks |
760 | ); |
761 | } |
762 | } |