Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
66.67% covered (warning)
66.67%
96 / 144
52.94% covered (warning)
52.94%
9 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResourceFile
66.67% covered (warning)
66.67%
96 / 144
52.94% covered (warning)
52.94%
9 / 17
187.93
0.00% covered (danger)
0.00%
0 / 1
 validate
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isPackage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 readVeronaMetaData
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 readVeronaMetadataV3
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
 getPlayerMetaElementV3
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getPlayerTitleV3
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 readVeronaMetadataV35
14.29% covered (danger)
14.29%
3 / 21
0.00% covered (danger)
0.00%
0 / 1
14.08
 getPreferredTranslationV35
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 readVeronaMetadataV4
81.82% covered (warning)
81.82%
18 / 22
0.00% covered (danger)
0.00%
0 / 1
5.15
 getPreferredTranslationV4
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
8.14
 getPlayerMetaElementV4
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 applyMeta
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 analyzeMeta
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 validatePackage
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
2.26
 getPackageContentPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 installPackage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 uninstallPackage
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/** @noinspection PhpUnhandledExceptionInspection */
3declare(strict_types=1);
4
5class ResourceFile extends File {
6  const type = 'Resource';
7  const canBeRelationSubject = false;
8  const canBeRelationObject = true;
9
10  protected function validate(): void {
11    parent::validate();
12
13    $this->id = strtoupper($this->name);
14
15    if (FileExt::has($this->getPath(), 'HTML')) {
16      $this->readVeronaMetaData();
17      $this->id = strtoupper($this->getVeronaModuleId() . '-' . $this->versionMayor . '.' . $this->versionMinor);
18    }
19
20    if ($this->isPackage()) {
21      $this->validatePackage();
22    }
23  }
24
25  public function isPackage(): bool {
26    return FileExt::has($this->getPath(), 'ITCR.ZIP');
27  }
28
29  // player is not it's own class, because player and other resources are stores in the same dir
30  private function readVeronaMetaData() {
31    if ($this->isValid() and $this->getContent()) {
32      $document = new DOMDocument();
33      $document->loadHTML($this->getContent(), LIBXML_NOERROR);
34
35      if ($metaV4Problem = $this->readVeronaMetadataV4($document)) {
36        if (!$this->readVeronaMetadataV35($document)) {
37          if (!$this->readVeronaMetadataV3($document)) {
38            $this->report('warning', $metaV4Problem);
39          }
40        }
41      }
42    }
43
44    if (!$this->getVersion()) {
45      list(
46        $this->veronaModuleId,
47        ,
48        $this->versionMayor,
49        $this->versionMinor,
50        $this->versionPatch,
51        $this->versionLabel,
52        ) = array_values(Version::guessFromFileName(basename($this->getPath())));
53
54      $this->report('warning', 'Metadata missing. Version guessed from Filename.');
55    }
56
57    $this->applyMeta();
58    $this->analyzeMeta();
59  }
60
61  /**
62   * This was a temporary way of defining meta-data of a player until in Verona4 a definitive way was defined. Since
63   * we produced a bunch of player-versions including this kind of metadata we should support it as long as we support
64   * Verona3.
65   *
66   * @deprecated
67   */
68  private function readVeronaMetadataV3(DOMDocument $document): bool {
69    $this->label = $this->getPlayerTitleV3($document);
70
71    $meta = $this->getPlayerMetaElementV3($document);
72    if (!$meta or !$meta->getAttribute('content')) {
73      return false;
74    }
75
76    $this->veronaModuleId = $meta->getAttribute('content');
77
78    list(
79      $this->versionMayor,
80      $this->versionMinor,
81      $this->versionPatch,
82      $this->versionLabel
83      ) = array_values(Version::split($meta->getAttribute('data-version')));
84
85    $this->veronaVersion = $meta->getAttribute('data-api-version');
86    $this->description = $meta->getAttribute('data-description');
87    $this->veronaModuleType = 'player';
88
89    $this->report('warning', 'Metadata in meta-tag is deprecated!');
90    return true;
91  }
92
93  private function getPlayerMetaElementV3(DOMDocument $document): ?DOMElement {
94    $metaElements = $document->getElementsByTagName('meta');
95    foreach ($metaElements as $metaElement) {
96      /* @var $metaElement DOMElement */
97      if ($metaElement->getAttribute('name') == 'application-name') {
98        return $metaElement;
99      }
100    }
101    return null;
102  }
103
104  private function getPlayerTitleV3(DOMDocument $document): string {
105    $titleElements = $document->getElementsByTagName('title');
106    if (!count($titleElements)) {
107      return '';
108    }
109    $titleElement = $titleElements[0];
110    /* @var $titleElement DOMElement */
111    return $titleElement->textContent;
112  }
113
114  /**
115   * This was another temporary way of defining meta-data of a player until in Verona4 a definitive way was defined.
116   * Since we produced a bunch of player-versions including this kind of metadata we should support it as long as
117   * we support Verona3.
118   *
119   * @deprecated
120   */
121  private function readVeronaMetadataV35(DOMDocument $document): bool {
122    $metaElem = $this->getPlayerMetaElementV4($document);
123    if (!$metaElem) {
124      return false;
125    }
126    try {
127      $meta = JSON::decode($metaElem->textContent, true);
128    } catch (Exception) {
129      return false;
130    }
131    if ($meta["@context"] !== "https://w3id.org/iqb/verona-modules") {
132      return false;
133    }
134    $this->label = $this->getPreferredTranslationV35($meta['name']);
135    $this->description = $this->getPreferredTranslationV35($meta['description']);
136    $this->veronaModuleId = $meta['@id'];
137    $this->veronaVersion = $meta['apiVersion'];
138    list(
139      $this->versionMayor,
140      $this->versionMinor,
141      $this->versionPatch,
142      $this->versionLabel
143      ) = array_values(Version::split($meta['version']));
144    $this->veronaModuleType = $meta['@type'];
145
146    $this->report('warning', 'Deprecated meta-data-format found!');
147    return true;
148  }
149
150  private function getPreferredTranslationV35(?array $multiLangItem): string {
151    if (!$multiLangItem or !count($multiLangItem)) {
152      return '';
153    }
154
155    $first = array_keys($multiLangItem)[0];
156    return $multiLangItem['de']
157      ?? $multiLangItem['en']
158      ?? $multiLangItem[$first];
159  }
160
161  private function readVeronaMetadataV4(DOMDocument $document): ?string {
162    $metaElem = $this->getPlayerMetaElementV4($document);
163    if (!$metaElem) {
164      return "No Metadata Element";
165    }
166    try {
167      $meta = JSON::decode($metaElem->textContent, true);
168    } catch (Exception $e) {
169      return "Could not read metadata: {$e->getMessage()}";
170    }
171    if (!isset($meta['$schema'])) {
172      return "Could not read metadata: \$schema missing";
173    }
174    if ($meta['$schema'] !== "https://raw.githubusercontent.com/verona-interfaces/metadata/master/verona-module-metadata.json") {
175      return "Wrong metadata-schema: {$meta['$schema']}";
176    }
177    $this->label = $this->getPreferredTranslationV4($meta['name']);
178    $this->description = $this->getPreferredTranslationV4($meta['description']);
179    $this->veronaModuleId = $meta['id'];
180    $this->veronaVersion = $meta['specVersion'];
181    list(
182      $this->versionMayor,
183      $this->versionMinor,
184      $this->versionPatch,
185      $this->versionLabel
186      ) = array_values(Version::split($meta['version']));
187    $this->veronaModuleType = $meta['type'];
188    return null;
189  }
190
191  private function getPreferredTranslationV4(?array $multiLangItem): string {
192    if (!$multiLangItem or !count($multiLangItem)) {
193      return '';
194    }
195
196    foreach ($multiLangItem as $entry) {
197      if ($entry['lang'] == 'de') return $entry['value'];
198    }
199    foreach ($multiLangItem as $entry) {
200      if ($entry['lang'] == 'en') return $entry['value'];
201    }
202
203    return $multiLangItem[0]['value'];
204  }
205
206  private function getPlayerMetaElementV4(DOMDocument $document): ?DOMElement {
207    $metaElements = $document->getElementsByTagName('script');
208    foreach ($metaElements as $metaElement) {
209      /* @var $metaElement DOMElement */
210      if ($metaElement->getAttribute('type') == 'application/ld+json') {
211        return $metaElement;
212      }
213    }
214    return null;
215  }
216
217  private function applyMeta(): void {
218    if (!$this->label and $this->veronaModuleId) {
219      $this->label = $this->veronaModuleId;
220      $this->label .= $this->getVersion() ? '-' . $this->getVersion() : '';
221    }
222  }
223
224  private function analyzeMeta(): void {
225    if ($this->veronaVersion) {
226      $this->report('info', "Verona-Version: $this->veronaVersion");
227    }
228
229    if ($this->veronaModuleId and $this->getVersion()) {
230      $recommendedFilename = "$this->veronaModuleId-{$this->getVersionMayorMinor()}.html";
231      if ($recommendedFilename != $this->name) {
232        $this->report('warning', "Non-Standard-Filename: `$this->veronaModuleId-{$this->getVersionMayorMinor()}.html` expected.");
233      }
234    }
235  }
236
237  public function validatePackage(): void {
238    try {
239      $meta = ZIP::readMeta($this->getPath());
240
241      $this->description = $meta['comment'] ?? '';
242
243      $this->report('info', "Contains {$meta['count']} files.");
244
245    } catch (Exception $e) {
246      $this->report('error', "Could not read archive: {$e->getMessage()}");
247    }
248  }
249
250  private function getPackageContentPath(): string {
251    return dirname($this->getPath()) . '/' . basename($this->getName(), '.itcr.zip');
252  }
253
254  public function installPackage(): void {
255    $contentsDirName = $this->getPackageContentPath();
256
257    try {
258      $this->uninstallPackage();
259
260    } catch (Exception $e) {
261      $this->report('error', "Could not delete package files: {$e->getMessage()}");
262      return;
263    }
264
265    try {
266      ZIP::extract($this->getPath(), $contentsDirName);
267
268    } catch (Exception $e) {
269      $this->report('error', "Could not extract package: {$e->getMessage()}");
270      return;
271    }
272  }
273
274  public function uninstallPackage(): void {
275    $contentsDirName = $this->getPackageContentPath();
276    if (file_exists($contentsDirName)) {
277      if (is_dir($contentsDirName)) {
278        Folder::deleteContentsRecursive($contentsDirName);
279        rmdir($contentsDirName);
280      } else {
281        unlink($contentsDirName);
282      }
283    }
284  }
285}