Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
28.32% covered (danger)
28.32%
81 / 286
20.83% covered (danger)
20.83%
5 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
WorkspaceDAO
28.32% covered (danger)
28.32%
81 / 286
20.83% covered (danger)
20.83%
5 / 24
1008.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getWorkspaceId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWorkspaceName
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getGlobalIds
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 updateLoginSource
n/a
0 / 0
n/a
0 / 0
1
 addLoginSource
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
4
 deleteLoginSource
n/a
0 / 0
n/a
0 / 0
1
 storeFile
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
2
 deleteFile
n/a
0 / 0
n/a
0 / 0
1
 getFileById
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 updateUnitDefsAttachments
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 getAllFiles
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getFilesByNames
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getAllFilesWhere
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getFileRelations
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 getFiles
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 fetchFiles
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 resultRow2File
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
2
 getBlockedFiles
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
12
 storeRelations
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 getBookletResourcePaths
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 getWorkspaceHash
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setWorkspaceHash
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 setSysCheckModeAccordingToTT
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
3.07
 setSysCheckMode
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 fetchDependenciesForFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getDependentFilesByTypes
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/** @noinspection PhpUnhandledExceptionInspection */
3declare(strict_types=1);
4
5class WorkspaceDAO extends DAO {
6  private int $workspaceId;
7  private string $workspacePath;
8
9  public function __construct(int $workspaceId, string $workspacePath) {
10    parent::__construct();
11    $this->workspaceId = $workspaceId;
12    $this->workspacePath = $workspacePath;
13  }
14
15  public function getWorkspaceId(): int
16  {
17    return $this->workspaceId;
18  }
19
20  public function getWorkspaceName(): string {
21    $workspace = $this->_(
22      'select workspaces.name 
23            from workspaces
24            where workspaces.id=:workspace_id',
25      [':workspace_id' => $this->workspaceId]
26    );
27
28    if ($workspace == null) {
29      throw new HttpError("Workspace `$this->workspaceId` not found", 404); // @codeCoverageIgnore
30    }
31
32    return $workspace['name'];
33  }
34
35  public function getGlobalIds(): array {
36    $globalIds = $this->_("
37      select
38        globalIds.id as id,
39        source,
40        workspace_id,
41        workspaces.name as workspace_name,
42        globalIds.type as type
43      from (
44        select name as id, source, workspace_id, 'login' as type from logins
45        union
46        select group_name as id, source, workspace_id, 'group' as type from logins group by group_name, source, workspace_id
47      ) as globalIds
48      left join workspaces on workspace_id
49      ",
50      [],
51      true
52    );
53
54    $arr = [];
55
56    foreach ($globalIds as $globalId) {
57      $arr[$globalId['workspace_id']][$globalId['source']][$globalId['type']][] = $globalId['id'];
58      $arr[$globalId['workspace_id']]['/name/'] = $globalId['workspace_name'];
59    }
60
61    return $arr;
62  }
63
64  /**
65   * @codeCoverageIgnore
66   */
67  public function updateLoginSource(string $source, LoginArray $logins): array {
68    $deleted = $this->deleteLoginSource($source);
69    $added = $this->addLoginSource($source, $logins);
70    return [$deleted, $added];
71  }
72
73
74  // TODO unit-test
75  public function addLoginSource(string $source, LoginArray $logins): int {
76
77    // one source could contain 10ks of logins. For the sake of performance we use one statement to insert them all
78    // and plain foreach and string-concatenation to build the query.
79
80    $sql = 'insert into logins (
81      name,
82      mode,
83      workspace_id,
84      codes_to_booklets,
85      group_name,
86      group_label,
87      custom_texts,
88      password,
89      source,
90      valid_from,
91      valid_to,
92      valid_for,
93      monitors
94    ) values';
95
96    foreach ($logins as $login) {
97      /* @var $login Login */
98      $loginValues = array_map(
99        function (string|int|null $v): string|int {
100          if ($v == null) return 'null';
101          if (is_string($v)) return $this->pdoDBhandle->quote($v);
102          return $v;
103        },
104        [
105          $login->getName(),
106          $login->getMode(),
107          $this->workspaceId,
108          json_encode($login->getBooklets()),
109          $login->getGroupName(),
110          $login->getGroupLabel(),
111          json_encode($login->getCustomTexts()),
112          Password::encrypt($login->getPassword(), 't', true),
113          $source,
114          TimeStamp::toSQLFormat($login->getValidFrom()),
115          TimeStamp::toSQLFormat($login->getValidTo()),
116          $login->getValidForMinutes(),
117          json_encode($login->getProfiles())
118        ]
119      );
120      $sql .= '(' . implode(', ', $loginValues) . '),';
121    }
122    $sql = rtrim($sql, ",");
123
124    $this->_($sql, [], true);
125
126    $this->setSysCheckModeAccordingToTT($logins);
127
128    return count($logins->asArray());
129  }
130
131  /**
132   * @codeCoverageIgnore
133   */
134  public function deleteLoginSource(string $source): int {
135    $this->_(
136      'delete from logins where source = :source and workspace_id = :ws_id',
137      [
138        ':source' => $source,
139        ':ws_id' => $this->workspaceId
140      ]
141    );
142    return $this->lastAffectedRows;
143  }
144
145  public function storeFile(File $file): void {
146    $this->_("replace into files (
147                    workspace_id,
148                    name,
149                    id,
150                    version_mayor,
151                    version_minor,
152                    version_patch,
153                    version_label,
154                    label,
155                    description,
156                    type,
157                    verona_module_type,
158                    verona_version,
159                    verona_module_id,
160                    is_valid,
161                    validation_report,
162                    size,
163                    modification_ts,
164                    context_data
165                ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
166      [
167        $this->workspaceId,
168        $file->getName(),
169        $file->getId(),
170        $file->getVersionMayor(),
171        $file->getVersionMinor(),
172        $file->getVersionPatch(),
173        $file->getVersionLabel(),
174        $file->getLabel(),
175        $file->getDescription(),
176        $file->getType(),
177        $file->getVeronaModuleType(),
178        $file->getVeronaVersion(),
179        $file->getVeronaModuleId(),
180        $file->isValid() ? 1 : 0,
181        serialize($file->getValidationReport()),
182        $file->getSize(),
183        TimeStamp::toSQLFormat($file->getModificationTime()),
184        serialize($file->getContextData())
185      ]
186    );
187  }
188
189  /**
190   * @codeCoverageIgnore
191   */
192  public function deleteFile(File $file): void {
193    $this->_("delete from files where workspace_id = ? and name = ? and type = ?", [$this->workspaceId, $file->getName(), $file->getType()]);
194  }
195
196  public function getFileById(string $fileId, string $type): ?File {
197    $fileData = $this->_(
198      "select
199                    name,
200                    id,
201                    label,
202                    type,
203                    description,
204                    is_valid,
205                    validation_report,
206                    size,
207                    modification_ts,
208                    version_mayor,
209                    version_minor,
210                    version_patch,
211                    version_label,
212                    verona_module_id,
213                    verona_module_type,
214                    verona_version,
215                    context_data
216                from
217                    files
218                where
219                    workspace_id = ? and id = ? and type = ?",
220      [
221        $this->workspaceId,
222        $fileId,
223        $type
224      ]
225    );
226
227    return $fileData ? $this->resultRow2File($fileData, []) : null;
228  }
229
230  public function updateUnitDefsAttachments(string $bookletName, array $attachments): void {
231    $this->_(
232      'delete from unit_defs_attachments where workspace_id = :workspace_id and booklet_name = :booklet_name;',
233      [
234        ':workspace_id' => $this->workspaceId,
235        ':booklet_name' => $bookletName
236      ]
237    );
238
239    foreach ($attachments as $requestedAttachment) {
240      /* @var RequestedAttachment $requestedAttachment */
241
242      $this->_(
243        'replace into unit_defs_attachments(workspace_id, booklet_name, unit_name, variable_id, attachment_type)
244                    values(:workspace_id, :booklet_name, :unit_name, :variable_id, :attachment_type)',
245        [
246          ':workspace_id' => $this->workspaceId,
247          ':booklet_name' => $bookletName,
248          ':unit_name' => $requestedAttachment->unitName,
249          ':variable_id' => $requestedAttachment->variableId,
250          ':attachment_type' => $requestedAttachment->attachmentType
251        ]);
252    }
253  }
254
255  public function getAllFiles(): array {
256    $sql = "
257            select
258                name,
259                type,
260                id,
261                label,
262                description,
263                is_valid,
264                validation_report,
265                size,
266                modification_ts,
267                version_mayor,
268                version_minor,
269                version_patch,
270                version_label,
271                verona_module_id,
272                verona_module_type,
273                verona_version,
274                context_data
275            from files
276                where workspace_id = ?";
277    $replacements = [$this->workspaceId];
278
279    return $this->fetchFiles($sql, $replacements);
280  }
281
282  public function getFilesByNames(array $names): array {
283    $sql = "
284            select
285                name,
286                type,
287                id,
288                label,
289                description,
290                is_valid,
291                validation_report,
292                size,
293                modification_ts,
294                version_mayor,
295                version_minor,
296                version_patch,
297                version_label,
298                verona_module_id,
299                verona_module_type,
300                verona_version,
301                context_data
302            from files
303                where workspace_id = ? and
304                name in (" . implode(',', array_map(fn ($name) => '?', $names)) . ')';
305
306    $replacements = [
307      $this->workspaceId,
308      ...$names
309    ];
310
311    return $this->fetchFiles($sql, $replacements, true);
312  }
313
314  /** @param array $conditions list('column' => value)
315   * @return array ['filetype' => File[]]*/
316  public function getAllFilesWhere(array $conditions): array {
317    $sql = "
318            select
319                name,
320                type,
321                id,
322                label,
323                description,
324                is_valid,
325                validation_report,
326                size,
327                modification_ts,
328                version_mayor,
329                version_minor,
330                version_patch,
331                version_label,
332                verona_module_id,
333                verona_module_type,
334                verona_version,
335                context_data
336            from files
337                where workspace_id = ?";
338    $replacements = [$this->workspaceId];
339
340    foreach ($conditions as $condition => $value) {
341      $sql .= " and $condition = ?";
342      $replacements[] = $value;
343    }
344
345    return $this->fetchFiles($sql, $replacements);
346  }
347
348  public function getFileRelations(string $name, string $type): array {
349    $relations = $this->_("
350            select
351                object_type,
352                object_name,
353                relationship_type,
354                id as object_id
355            from
356                file_relations
357                left join files
358                    on file_relations.workspace_id = files.workspace_id
359                       and file_relations.object_name = files.name
360                       and file_relations.object_type = files.type
361            where
362                files.workspace_id = ?
363                    and subject_name = ?
364                    and subject_type = ?",
365      [$this->workspaceId, $name, $type],
366      true
367    );
368
369    return array_map(
370      function(array $r): FileRelation {
371        return new FileRelation(
372          $r['object_type'],
373          $r['object_name'],
374          constant("FileRelationshipType::{$r['relationship_type']}"),
375          null,
376          $r['object_id']
377        );
378      },
379      $relations
380    );
381  }
382
383  public function getFiles(array $localPaths, bool $includeInvalid = false): array {
384    $replacements = [$this->workspaceId];
385    $validFilePaths = 0;
386
387    foreach ($localPaths as $fileLocalPath) {
388      $partParts = explode('/', $fileLocalPath, 2);
389      if (count($partParts) == 2) {
390        list($replacements[], $replacements[]) = $partParts;
391        $validFilePaths++;
392      }
393    }
394
395    $filePathCondition = implode(' or ', array_fill(0, $validFilePaths, '(type = ? and name = ?)'));
396    $filePathCondition = $filePathCondition ? "and ($filePathCondition)" : '';
397
398    $andIsValid = $includeInvalid ? '' : ' and files.is_valid';
399
400    $sql = "select distinct
401                    name,
402                    type,
403                    id,
404                    label,
405                    description,
406                    is_valid,
407                    validation_report,
408                    size,
409                    modification_ts,
410                    version_mayor,
411                    version_minor,
412                    version_patch,
413                    version_label,
414                    verona_module_id,
415                    verona_module_type,
416                    verona_version,
417                    context_data
418                from files
419                where
420                    files.workspace_id = ?
421                    $andIsValid
422                    $filePathCondition";
423
424    return $this->fetchFiles($sql, $replacements);
425  }
426
427  private function fetchFiles($sql, $replacements, bool $getDependencies = false): array {
428    $files = [];
429    foreach ($this->_($sql, $replacements, true) as $row) {
430      $files[$row['type']] ??= [];
431      // $relations = $this->getFileRelations($workspaceId, $row['name'], $row['type']);
432      if ($getDependencies) {
433        $dependencies = $this->fetchDependenciesForFile($row['name']);
434      }
435      $files[$row['type']][$row['name']] = $this->resultRow2File($row, $dependencies ?? []);
436    }
437    return $files;
438  }
439
440  private function resultRow2File(array $row, array $relations): ?File {
441    return File::get(
442      new FileData(
443        "$this->workspacePath/{$row['type']}/{$row['name']}",
444        $row['type'],
445        $row['id'],
446        $row['label'],
447        $row['description'],
448        !!$row['is_valid'],
449        unserialize($row['validation_report']),
450        $relations,
451        TimeStamp::fromSQLFormat($row['modification_ts']),
452        $row['size'],
453        unserialize($row['context_data']),
454        $row['verona_module_type'],
455        $row['verona_module_id'],
456        $row['version_mayor'],
457        $row['version_minor'],
458        $row['version_patch'],
459        $row['version_label'],
460        $row['verona_version']
461      ),
462      $row['type']
463    );
464  }
465
466  public function getBlockedFiles(array $files): array {
467    if (!count($files)) {
468      return [];
469    }
470
471    $replacements = [
472      ':ws_id' => $this->workspaceId
473    ];
474    $conditions = [];
475    $i = 0;
476    foreach ($files as $file) {
477      $i++;
478      $replacements[":type_$i"] = $file->getType();
479      $replacements[":name_$i"] = $file->getName();
480      $conditions[] = "(object_type = :type_$i and object_name = :name_$i)";
481    }
482
483    $selectedFilesConditions = implode(' or ', $conditions);
484
485    $sql = "with recursive affected_files as (
486                    -- base/first case that initializes the recursion
487                    select
488                        subject_type as object_type,
489                        subject_name as object_name,
490                        object_type || '/' || object_name as ancestor
491                    from file_relations
492                    where
493                        workspace_id = :ws_id and ($selectedFilesConditions)
494                
495                    union all
496                
497                    -- recursive case
498                    select
499                        file_relations.subject_type as object_type,
500                        file_relations.subject_name as object_name,
501                        affected_files.ancestor
502                    from affected_files
503                        join file_relations
504                            on affected_files.object_name = file_relations.object_name
505                                and affected_files.object_type = file_relations.object_type
506                                and file_relations.workspace_id = :ws_id
507                )
508                select distinct
509                    affected_files.ancestor as file_local_path,
510                    object_type || '/' || object_name as blocked_by
511                from affected_files
512                where
513                    not ($selectedFilesConditions)";
514
515    $result = $this->_($sql, $replacements, true);
516
517    return array_reduce(
518      $result,
519      function(array $agg, array $row) {
520        $agg[$row['file_local_path']] = $row['blocked_by'];
521        return $agg;
522      },
523      []
524    );
525  }
526
527  public function storeRelations(File $file): array {
528    $unresolvedRelations = [];
529    $updatedRelations = [];
530
531    foreach ($file->getRelations() as $relation) {
532      /* @var $relation FileRelation */
533
534      $relatedFile = $relation->getTarget();
535
536      if (!$relatedFile) {
537        $unresolvedRelations++;
538      }
539
540      $this->_(
541        "replace into file_relations (workspace_id, subject_name, subject_type, relationship_type, object_type, object_name)
542                values (?, ?, ?, ?, ?, ?);",
543        [
544          $this->workspaceId,
545          $file->getName(),
546          $file->getType(),
547          $relation->getRelationshipType()->name,
548          $relation->getTargetType(),
549          $relatedFile->getName()
550        ]
551      );
552
553      if ($this->lastAffectedRows != 1) {
554        $updatedRelations[] = $relation;
555      }
556    }
557
558    return [$unresolvedRelations, $updatedRelations];
559  }
560
561  public function getBookletResourcePaths(string $bookletFileName): array {
562      return
563        $this->_(
564          "select distinct
565            unitFiles.id,
566            resourceFiles.type,
567            resourceFiles.name,
568            unitNeedsResource.relationship_type
569          from file_relations as bookletContainsUnit
570            left join file_relations as unitNeedsResource
571              on bookletContainsUnit.workspace_id = unitNeedsResource.workspace_id
572                and bookletContainsUnit.object_name = unitNeedsResource.subject_name
573                and bookletContainsUnit.object_type = unitNeedsResource.subject_type
574--                and unitNeedsResource.relationship_type in('isDefinedBy', 'usesPlayer')
575                and unitNeedsResource.object_type = 'Resource'
576                and bookletContainsUnit.relationship_type = 'containsUnit'
577            left join files as resourceFiles
578              on resourceFiles.type = unitNeedsResource.object_type
579                and resourceFiles.name = unitNeedsResource.object_name
580                and resourceFiles.workspace_id = unitNeedsResource.workspace_id
581            left join files as unitFiles
582              on unitFiles.type = unitNeedsResource.subject_type
583                and unitFiles.name = unitNeedsResource.subject_name
584                and unitFiles.workspace_id = unitNeedsResource.workspace_id
585            where
586              bookletContainsUnit.subject_name = :booklet_file_name
587              and resourceFiles.workspace_id = :ws_id
588              and resourceFiles.is_valid = 1",
589            [
590              ':ws_id' => $this->workspaceId,
591              ':booklet_file_name' => $bookletFileName
592            ],
593          true
594        );
595    }
596
597  public function getWorkspaceHash(): string
598  {
599    return $this->_(
600      "select workspace_hash from workspaces where id = :ws_id",
601      [':ws_id' => $this->workspaceId]
602    )['workspace_hash'];
603  }
604
605  public function setWorkspaceHash(string $hash): void
606  {
607    $this->_(
608      "update workspaces set workspace_hash = :hash where id = :ws_id",
609      [':hash' => $hash, ':ws_id' => $this->workspaceId]
610    );
611  }
612
613  private function setSysCheckModeAccordingToTT(LoginArray $logins) {
614    $enableSysCheckMode = SysCheckMode::TEST;
615    /** @var Login $login */
616    foreach ($logins as $login) {
617      if ($login->getMode() == 'sys-check-login') {
618        $enableSysCheckMode = SysCheckMode::SYSCHECK;
619        break;
620      }
621    }
622    $this->_(
623      "update workspaces set content_type = '$enableSysCheckMode->value' where id = :ws_id",
624      [':ws_id' => $this->workspaceId],
625      true
626    );
627  }
628
629  public function setSysCheckMode(string $mode): void {
630    $this->_(
631      "update workspaces set content_type = :mode where id = :ws_id",
632      [
633        ':mode' => $mode,
634        ':ws_id' => $this->workspaceId
635      ],
636      true
637    );
638  }
639
640  public function fetchDependenciesForFile(string $name): ?array {
641    return $this->_(
642      "
643        -- base case that starts the recursion
644          with recursive dependencies as (
645            select subject_name, object_name, relationship_type
646            from file_relations
647            where subject_name = :name
648              
649            union all 
650            
651            -- recursive case
652            select fr.subject_name, fr.object_name, fr.relationship_type
653            from file_relations fr
654            inner join dependencies dep
655              on fr.subject_name = dep.object_name 
656          )
657          select distinct object_name, relationship_type
658          from dependencies;",
659      [
660        ':name' => $name,
661      ],
662      true
663    );
664  }
665
666  public function getDependentFilesByTypes(File $file, array $types = []): array {
667    $sql = "select
668      files.name,
669      files.type,
670      files.id,
671      files.label,
672      files.description,
673      files.is_valid,
674      files.validation_report,
675      files.size,
676      files.modification_ts,
677      files.version_mayor,
678      files.version_minor,
679      files.version_patch,
680      files.version_label,
681      files.verona_module_id,
682      files.verona_module_type,
683      files.verona_version,
684      files.context_data
685    from file_relations
686      left join files
687        on file_relations.workspace_id = files.workspace_id
688          and file_relations.subject_name = files.name
689          and file_relations.subject_type = files.type
690    where
691          files.workspace_id = :ws_id
692          and object_type = :file_type and object_name= :file_name";
693
694    $replacements = [
695      ':ws_id' => $this->workspaceId,
696      ':file_type' => $file->getType(),
697      ':file_name' => $file->getName()
698    ];
699
700    // add more conditions
701    if (!empty($types)) {
702      $conditions = [];
703      foreach ($types as $index => $type) {
704        $conditions[] = "files.type = :type$index";
705        $replacements[":type$index"] = $type;
706      }
707      $addCondition = ' and (' . implode(' or ', $conditions) . ')';
708      $sql .= $addCondition;
709    }
710
711    return $this->fetchFiles($sql, $replacements);
712  }
713}