Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
80.21% covered (warning)
80.21%
231 / 288
62.50% covered (warning)
62.50%
15 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
AdminDAO
80.21% covered (warning)
80.21%
231 / 288
62.50% covered (warning)
62.50%
15 / 24
71.16
0.00% covered (danger)
0.00%
0 / 1
 refreshAdminToken
n/a
0 / 0
n/a
0 / 0
1
 createAdminToken
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 getUserByNameAndPassword
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 deleteTokensByUser
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 storeToken
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 getAdmin
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
2.00
 deleteResultData
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 getWorkspaces
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 hasAdminAccessToWorkspace
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 hasMonitorAccessToWorkspace
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 getWorkspaceRole
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getTestSessions
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
6
 getUnitState
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
2.00
 getResponseReportData
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 getResponseDataParts
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getLogReportData
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getReviewReportData
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 getResultStats
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
1
 storeCommand
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
3.00
 getTest
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getGroup
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getAttachmentById
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getAttachments
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
56
 deleteAdminSession
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 doesWSwitTypeSyscheckExist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/** @noinspection PhpUnhandledExceptionInspection */
4declare(strict_types=1);
5
6class AdminDAO extends DAO {
7  /**
8   * @codeCoverageIgnore
9   */
10  public function refreshAdminToken(string $token): void {
11    $this->_(
12      'update admin_sessions
13            set valid_until =:value
14            where token =:token',
15      [
16        ':value' => TimeStamp::toSQLFormat(TimeStamp::expirationFromNow(0, $this->timeUserIsAllowedInMinutes)),
17        ':token' => $token
18      ]
19    );
20  }
21
22  public function createAdminToken(string $username, string $password, ?int $validTo = null): string | FailedLogin {
23    if ((strlen($username) == 0) or (strlen($username) > 50)) {
24      throw new Exception("Invalid Username `$username`", 400);
25    }
26
27    $user = $this->getUserByNameAndPassword($username, $password);
28
29    if (is_a($user, FailedLogin::class)) return $user;
30
31    $this->deleteTokensByUser((int) $user['id']);
32    $token = Token::generate('admin', $username);
33    $this->storeToken((int) $user['id'], $token, $validTo);
34
35    return $token;
36  }
37
38  private function getUserByNameAndPassword(string $userName, string $password): array | FailedLogin {
39    $usersOfThisName = $this->_(
40      'select * from users where users.name = :name',
41      [':name' => $userName],
42      true
43    );
44
45    $return = (!count($usersOfThisName)) ? FailedLogin::usernameNotFound : FailedLogin::wrongPassword;
46
47    // we always check at least one user to not leak the existence of username to time-attacks
48    $usersOfThisName = (!count($usersOfThisName)) ? [['password' => 'dummy']] : $usersOfThisName;
49
50    foreach ($usersOfThisName as $user) {
51      if (Password::verify($password, $user['password'], $this->passwordSalt)) {
52        return $user;
53      }
54    }
55
56    // obfuscate the time taken even more
57    usleep(rand(000000, 100000));
58    return $return;
59  }
60
61  private function deleteTokensByUser(int $userId): void {
62    $this->_(
63      'delete from admin_sessions where admin_sessions.user_id = :id',
64      [':id' => $userId]
65    );
66  }
67
68  private function storeToken(int $userId, string $token, ?int $validTo = null): void {
69    $validTo = $validTo ?? TimeStamp::expirationFromNow(0, $this->timeUserIsAllowedInMinutes);
70
71    $this->_(
72      'insert into admin_sessions (token, user_id, valid_until)
73            values(:token, :user_id, :valid_until)',
74      [
75        ':token' => $token,
76        ':user_id' => $userId,
77        ':valid_until' => TimeStamp::toSQLFormat($validTo)
78      ]
79    );
80  }
81
82  public function getAdmin(string $token): Admin {
83    $admin = $this->_(
84      'select
85        users.id,
86        users.name,
87        users.email,
88        users.is_superadmin,
89        users.pw_set_by_admin,
90        admin_sessions.valid_until
91      from users
92            inner join admin_sessions on users.id = admin_sessions.user_id
93            where admin_sessions.token=:token',
94      [':token' => $token]
95    );
96
97    if (!$admin) {
98      throw new HttpError("Token not valid! ($token)", 403);
99    }
100
101    TimeStamp::checkExpiration(0, TimeStamp::fromSQLFormat($admin['valid_until']));
102
103    return new Admin(
104      $admin['id'],
105      $admin['name'],
106      $admin['email'] ?? '',
107      !!$admin['is_superadmin'],
108      $token,
109      (bool) $admin['pw_set_by_admin']
110    );
111  }
112
113  public function deleteResultData(int $workspaceId, string $groupName): void {
114    $this->_(
115      "delete from login_sessions where group_name = :group_name and workspace_id = :workspace_id",
116      [
117        ':workspace_id' => $workspaceId,
118        ':group_name' => $groupName
119      ]
120    );
121  }
122
123  /** @return WorkspaceData[] */
124  public function getWorkspaces(string $token): array {
125    $workspaces = $this->_(
126      'select
127        workspaces.id,
128        workspaces.name,
129        workspace_users.role
130      from workspaces
131        inner join workspace_users on workspaces.id = workspace_users.workspace_id
132        inner join users on workspace_users.user_id = users.id
133        inner join admin_sessions on  users.id = admin_sessions.user_id
134      where
135        admin_sessions.token =:token',
136      [':token' => $token],
137      true
138    );
139    return array_map(
140      function (array $ws): WorkspaceData {
141        return new WorkspaceData($ws['id'], $ws['name'], $ws['role']);
142      },
143      $workspaces
144    );
145  }
146
147  public function hasAdminAccessToWorkspace(string $token, int $workspaceId): bool {
148    $data = $this->_(
149      'select workspaces.id from workspaces
150                inner join workspace_users on workspaces.id = workspace_users.workspace_id
151                inner join users on workspace_users.user_id = users.id
152                inner join admin_sessions on  users.id = admin_sessions.user_id
153                where admin_sessions.token =:token and workspaces.id = :wsId',
154      [
155        ':token' => $token,
156        ':wsId' => $workspaceId
157      ]
158    );
159
160    return $data != false;
161  }
162
163  public function hasMonitorAccessToWorkspace(string $token, int $workspaceId): bool {
164    $data = $this->_(
165      'select workspaces.id from workspaces
166                inner join login_sessions on workspaces.id = login_sessions.workspace_id
167                inner join person_sessions on person_sessions.login_sessions_id = login_sessions.id
168                where person_sessions.token =:token and workspaces.id = :wsId',
169      [
170        ':token' => $token,
171        ':wsId' => $workspaceId
172      ]
173    );
174
175    return $data != false;
176  }
177
178  public function getWorkspaceRole(string $token, int $workspaceId): string {
179    $user = $this->_(
180      'select workspace_users.role from workspaces
181                inner join workspace_users on workspaces.id = workspace_users.workspace_id
182                inner join users on workspace_users.user_id = users.id
183                inner join admin_sessions on  users.id = admin_sessions.user_id
184                where admin_sessions.token =:token and workspaces.id = :wsId',
185      [
186        ':token' => $token,
187        ':wsId' => $workspaceId
188      ]
189    );
190
191    return $user['role'] ?? '';
192  }
193
194  public function getTestSessions(int $workspaceId, array $groups): SessionChangeMessageArray {
195    $groupSelector = false;
196    if (count($groups)) {
197      $groupSelector = "'" . implode("', '", $groups) . "'";
198    }
199
200    $modeSelector = "'" . implode("', '", Mode::getByCapability('monitorable')) . "'";
201
202    $sql = 'SELECT
203                 person_sessions.id as "person_id",
204                 login_sessions.name as "loginName",
205                 login_sessions.id as "login_sessions_id",
206                 logins.name,
207                 logins.mode,
208                 logins.group_name,
209                 logins.group_label,
210                 logins.workspace_id,
211                 person_sessions.code,
212                 person_sessions.name_suffix,
213                 tests.id as "test_id",
214                 tests.name as "booklet_name",
215                 tests.locked,
216                 tests.running,
217                 tests.laststate as "testState",
218                 tests.timestamp_server as "test_timestamp_server"
219            FROM person_sessions
220                 LEFT JOIN tests ON person_sessions.id = tests.person_id
221                 LEFT JOIN login_sessions ON login_sessions.id = person_sessions.login_sessions_id
222                 LEFt JOIN logins on logins.name = login_sessions.name
223            WHERE
224                login_sessions.workspace_id = :workspaceId
225                AND tests.id is not null'
226      . ($groupSelector ? " AND logins.group_name IN ($groupSelector)" : '')
227      . " AND logins.mode IN ($modeSelector)";
228
229    $testSessionsData = $this->_($sql, [':workspaceId' => $workspaceId], true);
230
231    $sessionChangeMessages = new SessionChangeMessageArray();
232
233    foreach ($testSessionsData as $testSession) {
234      $testState = $this->getTestFullState($testSession);
235
236      $sessionChangeMessage = SessionChangeMessage::session(
237        (int) $testSession['test_id'],
238        new PersonSession(
239          new LoginSession(
240            (int) $testSession['login_sessions_id'],
241            '',
242            '',
243            new Login(
244              $testSession['name'],
245              '',
246              $testSession['mode'],
247              $testSession['group_name'],
248              $testSession['group_label'],
249              [],
250              (int) ['workspace_id']
251            )
252          ),
253          new Person(
254            (int) $testSession['person_id'],
255            '',
256            $testSession['code'],
257            (string) $testSession['name_suffix'],
258          )
259        ),
260        TimeStamp::fromSQLFormat($testSession['test_timestamp_server']),
261      );
262      $sessionChangeMessage->setTestState(
263        $testState,
264        $testSession['booklet_name'] ?? ""
265      );
266
267      $currentUnitName = $testState['CURRENT_UNIT_ID'] ?? null;
268
269      if ($currentUnitName) {
270        $currentUnitState = $this->getUnitState((int) $testSession['test_id'], $currentUnitName);
271
272        if ($currentUnitState) {
273          $sessionChangeMessage->setUnitState($currentUnitName, (array) $currentUnitState);
274        }
275      }
276
277      $sessionChangeMessages->add($sessionChangeMessage);
278    }
279
280    return $sessionChangeMessages;
281  }
282
283  private function getUnitState(int $testId, string $unitName): stdClass {
284    $unitData = $this->_("
285      select
286          laststate
287      from
288          units
289      where
290          units.booklet_id = :testId
291          and units.name = :unitName",
292      [
293        ':testId' => $testId,
294        ':unitName' => $unitName
295      ]
296    );
297
298    if (!$unitData) {
299      return (object) [];
300    }
301
302    $state = JSON::decode($unitData['laststate'], true) ?? (object) [];
303
304    return (object) $state ?? (object) [];
305  }
306
307  public function getResponseReportData($workspaceId, $groups): ?array {
308    $groupsPlaceholders = implode(',', array_fill(0, count($groups), '?'));
309    $bindParams = array_merge([$workspaceId], $groups);
310
311    // TODO: use data class
312    $data = $this->_(<<<EOT
313      select
314        login_sessions.group_name as groupname,
315        login_sessions.name as loginname,
316        person_sessions.name_suffix as code,
317        tests.name as bookletname,
318        units.name as unitname,
319        units.laststate,
320        units.id as unit_id,
321        units.original_unit_id as originalUnitId
322      from
323        login_sessions
324          inner join person_sessions on login_sessions.id = person_sessions.login_sessions_id
325          inner join tests on person_sessions.id = tests.person_id
326          inner join units on tests.id = units.booklet_id
327      where
328        login_sessions.workspace_id = ?
329          and login_sessions.group_name in ($groupsPlaceholders)
330          and tests.id is not null
331      EOT,
332      $bindParams,
333      true
334    );
335
336    foreach ($data as $index => $row) {
337      $data[$index]['responses'] = $this->getResponseDataParts((int) $row['unit_id']);
338      unset($data[$index]['unit_id']);
339    }
340
341    return $data;
342  }
343
344  public function getResponseDataParts(int $unitId): array {
345    $data = $this->_(
346      'select
347         part_id as id,
348         content,
349         ts,
350         response_type as responseType
351       from
352         unit_data
353       where
354         unit_id = :unit_id',
355      [':unit_id' => $unitId],
356      true
357    );
358    foreach ($data as $index => $row) {
359      $data[$index]['ts'] = (int) $row['ts'];
360    }
361    return $data;
362  }
363
364  public function getLogReportData($workspaceId, $groups): ?array {
365    $groupsPlaceholders = implode(',', array_fill(0, count($groups), '?'));
366    $bindParams = array_merge([$workspaceId], $groups, [$workspaceId], $groups);
367
368    // TODO: use data class
369    return $this->_("
370        SELECT
371                  login_sessions.group_name as groupname,
372            login_sessions.name as loginname,
373            person_sessions.name_suffix as code,
374            tests.name as bookletname,
375            units.name as unitname,
376            units.original_unit_id as originalUnitId,
377                    unit_logs.timestamp,
378            unit_logs.logentry
379              FROM
380                  login_sessions,
381            person_sessions,
382            tests,
383            units,
384            unit_logs
385              WHERE
386            login_sessions.workspace_id = ? AND
387            login_sessions.group_name IN ($groupsPlaceholders) AND
388            login_sessions.id = person_sessions.login_sessions_id AND
389            person_sessions.id = tests.person_id AND
390            tests.id = units.booklet_id AND
391            units.id = unit_logs.unit_id
392        
393        UNION ALL
394        
395        SELECT
396                    login_sessions.group_name as groupname,
397            login_sessions.name as loginname,
398            person_sessions.name_suffix as code,
399            tests.name as bookletname,
400            '' as unitname,
401            '' as originalUnitId,
402            test_logs.timestamp,
403            test_logs.logentry
404              FROM
405            login_sessions,
406            person_sessions,
407            tests,
408            test_logs
409        WHERE
410            login_sessions.workspace_id = ? AND
411            login_sessions.group_name IN ($groupsPlaceholders) AND
412            login_sessions.id = person_sessions.login_sessions_id AND
413            person_sessions.id = tests.person_id AND
414            tests.id = test_logs.booklet_id
415            ",
416      $bindParams,
417      true
418    );
419  }
420
421  public function getReviewReportData($workspaceId, $groups): ?array {
422    $groupsPlaceholders = implode(',', array_fill(0, count($groups), '?'));
423    $bindParams = array_merge([$workspaceId], $groups, [$workspaceId], $groups);
424
425    // TODO: use data class
426    return $this->_(
427      "
428      SELECT
429        login_sessions.group_name as groupname,
430        login_sessions.name as loginname,
431        person_sessions.name_suffix as code,
432        tests.name as bookletname,
433        units.name as unitname,
434        unit_reviews.priority,
435        unit_reviews.categories,
436        unit_reviews.reviewtime,
437        unit_reviews.entry,
438        unit_reviews.page,
439        unit_reviews.pagelabel,
440        units.original_unit_id as originalUnitId,
441        unit_reviews.user_agent as userAgent
442            FROM
443        login_sessions,
444        person_sessions,
445        tests,
446        units,
447        unit_reviews
448            WHERE
449        login_sessions.workspace_id = ? AND
450        login_sessions.group_name IN ($groupsPlaceholders) AND
451        login_sessions.id = person_sessions.login_sessions_id AND
452        person_sessions.id = tests.person_id AND
453        tests.id = units.booklet_id AND
454        units.id = unit_reviews.unit_id
455            
456            UNION ALL
457        
458      SELECT
459        login_sessions.group_name as groupname,
460        login_sessions.name as loginname,
461        person_sessions.name_suffix as code,
462        tests.name as bookletname,
463        '' as unitname,
464        test_reviews.priority,
465        test_reviews.categories,
466        test_reviews.reviewtime,
467        test_reviews.entry,
468        null as page,
469        null as pagelabel,
470        '' as originalUnitId,
471        test_reviews.user_agent as userAgent
472            FROM
473        login_sessions,
474        person_sessions,
475        tests,
476        test_reviews
477            WHERE
478        login_sessions.workspace_id = ? AND
479        login_sessions.group_name IN ($groupsPlaceholders) AND
480        login_sessions.id = person_sessions.login_sessions_id AND
481        person_sessions.id = tests.person_id AND
482        tests.id = test_reviews.booklet_id
483            ",
484      $bindParams,
485      true
486    );
487  }
488
489  public function getResultStats(int $workspaceId): array {
490    $resultStats = $this->_('
491      select
492        group_name,
493        group_label,
494        count(*) as bookletsStarted,
495        min(num_units) as num_units_min,
496        max(num_units) as num_units_max,
497        sum(num_units) as num_units_total,
498        avg(num_units) as num_units_mean,
499        max(timestamp_server) as lastchange
500      from (
501        select
502          login_sessions.group_name,
503          group_label,
504          count(distinct units.id) as num_units,
505          max(tests.timestamp_server) as timestamp_server
506        from
507          tests
508          left join person_sessions 
509            on person_sessions.id = tests.person_id
510          inner join login_sessions
511            on login_sessions.id = person_sessions.login_sessions_id
512          left join units
513            on units.booklet_id = tests.id
514          left join unit_reviews
515            on units.id = unit_reviews.unit_id
516          left join test_reviews
517            on tests.id = test_reviews.booklet_id
518          left join login_session_groups on 
519            login_sessions.group_name = login_session_groups.group_name
520              and login_sessions.workspace_id = login_session_groups.workspace_id
521        where
522          login_sessions.workspace_id = :workspaceId
523          and (
524            tests.laststate is not null
525              or unit_reviews.entry is not null
526              or test_reviews.entry is not null
527          )
528          and tests.running = 1
529          group by tests.name, person_sessions.id, login_sessions.group_name, group_label
530      ) as byGroup
531      group by group_name',
532      [
533        ':workspaceId' => $workspaceId
534      ],
535      true
536    );
537
538    return array_map(function ($groupStats) {
539      return [
540        "groupName" => $groupStats["group_name"],
541        "groupLabel" => $groupStats["group_label"],
542        "bookletsStarted" => (int) $groupStats["bookletsStarted"],
543        "numUnitsMin" => (int) $groupStats["num_units_min"],
544        "numUnitsMax" => (int) $groupStats["num_units_max"],
545        "numUnitsTotal" => (int) $groupStats["num_units_total"],
546        "numUnitsAvg" => (float) $groupStats["num_units_mean"],
547        "lastChange" => TimeStamp::fromSQLFormat((string) $groupStats["lastchange"])
548      ];
549    }, $resultStats);
550  }
551
552  public function storeCommand(int $commanderId, int $testId, Command $command): int {
553    if ($command->getId() === -1) {
554      $maxId = $this->_("select max(id) as max from test_commands");
555      $commandId = isset($maxId['max']) ? (int) $maxId['max'] + 1 : 1;
556    } else {
557      $commandId = $command->getId();
558    }
559
560    $this->_("insert into test_commands (id, test_id, keyword, parameter, commander_id, timestamp)
561                values (:id, :test_id, :keyword, :parameter, :commander_id, :timestamp)",
562      [
563        ':id' => $commandId,
564        ':test_id' => $testId,
565        ':keyword' => $command->getKeyword(),
566        ':parameter' => json_encode($command->getArguments()),
567        ':commander_id' => $commanderId,
568        ':timestamp' => TimeStamp::toSQLFormat($command->getTimestamp())
569      ]
570    );
571
572    return $commandId;
573  }
574
575  // TODO use typed class instead of array
576  public function getTest(int $testId): ?array {
577    return $this->_(
578      'select tests.locked, tests.id, tests.laststate, tests.label from tests where tests.id=:id',
579      [':id' => $testId]
580    );
581  }
582
583  public function getGroup(string $groupName): ?Group {
584    $group = $this->_(
585      'select group_name, group_label
586                from logins
587                where group_name=:group_name
588                group by group_name, group_label',
589      [
590        ":group_name" => $groupName
591      ]
592    );
593    return ($group == null) ? null : new Group($group['group_name'], $group['group_label']);
594  }
595
596  // TODO unit-test
597  public function getAttachmentById(string $attachmentId): Attachment {
598    $attachments = $this->getAttachments(0, [], $attachmentId);
599
600    if (!count($attachments)) {
601      throw new HttpError("Attachment not found: `$attachmentId`", 404);
602    }
603
604    return $attachments[0];
605  }
606
607  // TODO unit-test
608  public function getAttachments(int $workspaceId = 0, array $groups = [], string $attachmentId = ''): array {
609    $selectors = [];
610    $replacements = [];
611
612    if (count($groups)) {
613      $selectors[] = "logins.group_name in (" . implode(',', array_fill(0, count($groups), '? ')) . ")";
614      $replacements = $groups;
615    }
616
617    if ($workspaceId) {
618      $selectors[] = "logins.workspace_id = ?";
619      $replacements[] = $workspaceId;
620    }
621
622    if ($attachmentId) {
623      list($testId, $unitName, $variableId) = Attachment::decodeId($attachmentId);
624      $selectors[] = "tests.id = ?";
625      $selectors[] = "unit_name = ?";
626      $selectors[] = "variable_id = ?";
627      $replacements[] = $testId;
628      $replacements[] = $unitName;
629      $replacements[] = $variableId;
630    }
631
632    $sql = "select
633                group_label as groupLabel,
634                logins.group_name as groupName,
635                logins.name as loginName,
636                name_suffix as nameSuffix,
637                tests.label as testLabel,
638                tests.id as testId,
639                tests.name as bookletName,
640                unit_name as unitName,
641                unit_name as unitLabel, -- TODO get real unitLabel
642                variable_id as variableId,
643                attachment_type as attachmentType,
644                unit_data.content as dataPartContent,
645                (tests.id || ':' || unit_name ||  ':' || variable_id) as attachmentId,
646                unit_data.ts as lastModified
647            from
648                unit_defs_attachments
649                left join tests on booklet_name = tests.name
650                left join person_sessions on tests.person_id = person_sessions.id
651                left join login_sessions on person_sessions.login_sessions_id = login_sessions.id
652                left join logins on logins.name = login_sessions.name
653                left join unit_data on part_id = (tests.id || ':' || unit_name || ':' || variable_id)
654            where " . implode(' and ', $selectors);
655
656    $attachments = $this->_($sql, $replacements, true);
657
658    $attachmentData = [];
659    foreach ($attachments as $attachment) {
660      $dataPart = JSON::decode($attachment['dataPartContent'], true);
661      $attachmentFileIds = $dataPart ? $dataPart[0]['value'] : [];
662
663      $attachmentData[] = new Attachment(
664        $attachment['attachmentId'],
665        $attachment['attachmentType'],
666        $attachment['dataPartContent'] ? explode(':', $attachmentFileIds[0])[0] : 'missing',
667        $attachmentFileIds,
668        $attachment['lastModified'],
669        $attachment['groupName'],
670        $attachment['groupLabel'],
671        $attachment['loginName'],
672        $attachment['nameSuffix'],
673        $attachment['testLabel'],
674        $attachment['bookletName'],
675        $attachment['unitLabel']
676      );
677    }
678    return $attachmentData;
679  }
680
681  public function deleteAdminSession(AuthToken $authToken): void {
682    $this->_('delete from admin_sessions where token =:token', [':token' => $authToken->getToken()]);
683  }
684
685  public function doesWSwitTypeSyscheckExist(): bool {
686    return $this->_("select count(*) as count from workspaces where content_type = 'sysCheck'")['count'] > 0;
687  }
688}