Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
72.40% covered (warning)
72.40%
299 / 413
38.89% covered (danger)
38.89%
7 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
SessionDAO
72.40% covered (warning)
72.40%
299 / 413
38.89% covered (danger)
38.89%
7 / 18
125.33
0.00% covered (danger)
0.00%
0 / 1
 getToken
95.45% covered (success)
95.45%
21 / 22
0.00% covered (danger)
0.00%
0 / 1
4
 getOrCreateLoginSession
n/a
0 / 0
n/a
0 / 0
2
 getLogin
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
4
 createLoginSession
96.15% covered (success)
96.15%
25 / 26
0.00% covered (danger)
0.00%
0 / 1
3
 getLoginSessionByToken
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
2
 createOrUpdatePersonSession
88.00% covered (warning)
88.00%
66 / 75
0.00% covered (danger)
0.00%
0 / 1
17.50
 getPersonSessionByToken
100.00% covered (success)
100.00%
40 / 40
100.00% covered (success)
100.00%
1 / 1
2
 getOrCreateGroupToken
90.91% covered (success)
90.91%
20 / 22
0.00% covered (danger)
0.00%
0 / 1
3.01
 groupTokenExists
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getTestStatus
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 personHasBooklet
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 ownsTest
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getTestsOfPerson
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
6
 deletePersonToken
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupMonitors
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getGroups
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 getDependantSessions
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getLoginSessions
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
3
 getSysChecksOfPerson
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/** @noinspection PhpUnhandledExceptionInspection */
3declare(strict_types=1);
4
5class 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}