Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
81.58% covered (warning)
81.58%
62 / 76
70.00% covered (warning)
70.00%
7 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
XMLFile
81.58% covered (warning)
81.58%
62 / 76
70.00% covered (warning)
70.00%
7 / 10
28.91
0.00% covered (danger)
0.00%
0 / 1
 validate
95.65% covered (success)
95.65%
22 / 23
0.00% covered (danger)
0.00%
0 / 1
4
 getXML
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 readMetadata
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 readSchema
56.25% covered (warning)
56.25%
9 / 16
0.00% covered (danger)
0.00%
0 / 1
9.01
 fallBackToCurrentSchemaVersion
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 validateAgainstSchema
62.50% covered (warning)
62.50%
10 / 16
0.00% covered (danger)
0.00%
0 / 1
3.47
 warnOnDeprecatedElements
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 importLibXmlErrors
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 xmlGetNodeContentIfPresent
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getRootTagName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/** @noinspection PhpUnhandledExceptionInspection */
3declare(strict_types=1);
4// TODO unit-tests
5
6class XMLFile extends File {
7  const type = 'xml';
8  const knownRootTags = ['Testtakers', 'Booklet', 'SysCheck', 'Unit'];
9
10  const deprecatedElements = [];
11
12  protected string $rootTagName = '';
13  protected ?array $schema;
14
15  private ?SimpleXMLElement $xml = null;
16
17  protected function validate(): void {
18    parent::validate();
19
20    if ($this->xml) {
21      return;
22    }
23
24    libxml_use_internal_errors(true);
25    libxml_clear_errors();
26
27    $xmlElem = simplexml_load_string($this->content);
28
29    if ($xmlElem === false) {
30      $this->importLibXmlErrors();
31      libxml_use_internal_errors(false);
32      $this->xml = new SimpleXMLElement('<error />');
33      return;
34    }
35
36    $this->xml = $xmlElem;
37    $this->rootTagName = $this->xml->getName();
38
39    if (!in_array($this->rootTagName, $this::knownRootTags)) {
40      $this->report('error', "Invalid root-tag: `$this->rootTagName`");
41      $this->importLibXmlErrors();
42      libxml_use_internal_errors(false);
43      return;
44    }
45
46    $this->readMetadata();
47
48    $this->importLibXmlErrors();
49    $this->validateAgainstSchema();
50    $this->warnOnDeprecatedElements();
51
52    libxml_use_internal_errors(false);
53  }
54
55  protected function getXML(): SimpleXMLElement {
56    parent::load();
57    return $this->xml;
58  }
59
60  private function readMetadata(): void {
61    $id = $this->xmlGetNodeContentIfPresent("/$this->rootTagName/Metadata/Id");
62    if ($id) {
63      $this->id = trim(strtoupper($id));
64    }
65
66    $this->label = $this->xmlGetNodeContentIfPresent("/$this->rootTagName/Metadata/Label");
67    $this->description = $this->xmlGetNodeContentIfPresent("/$this->rootTagName/Metadata/Description");
68  }
69
70  private function readSchema(): void {
71    // TODO support other ways of defining the schema (schemaLocation)
72
73    $schemaUrl = (string) $this->getXml()->attributes('xsi', true)->noNamespaceSchemaLocation;
74
75    if (!$schemaUrl) {
76      $this->fallBackToCurrentSchemaVersion('File has no link to XSD-Schema.');
77      return;
78    }
79
80    $this->schema = XMLSchema::parseSchemaUrl($schemaUrl);
81
82    if (!$this->schema) {
83      $this->report('error', 'File has no valid link to XSD-schema.');
84      return;
85    }
86
87    if ($this->schema['type'] !== $this->getRootTagName()) {
88      $this->report('error', 'File has no valid link to XSD-schema.');
89      return;
90    }
91
92    if (!$this->schema['version']) {
93      $this->fallBackToCurrentSchemaVersion("Version of XSD-schema missing.");
94      return;
95    }
96
97    if (!Version::isCompatible($this->schema['version'])) {
98      $this->fallBackToCurrentSchemaVersion("Outdated or wrong version of XSD-schema (`{$this->schema['version']}`).");
99    }
100  }
101
102  private function fallBackToCurrentSchemaVersion(string $message): void {
103    $currentVersion = SystemConfig::$system_version;
104    $this->report('warning', "$message Current version (`$currentVersion`) will be used instead.");
105    $this->schema = XMLSchema::getLocalSchema($this->getRootTagName());
106  }
107
108  private function validateAgainstSchema(): void {
109    $this->readSchema();
110    $schemaFilePath = XMLSchema::getSchemaFilePath($this->schema);
111    if (!$schemaFilePath) {
112      $this->fallBackToCurrentSchemaVersion("XSD-Schema (`{$this->schema['version']}`) could not be obtained.");
113      $schemaFilePath = XMLSchema::getSchemaFilePath($this->schema);
114    }
115
116    $xmlReader = new XMLReader();
117    $xmlReader->xml($this->getXML()->asXML());
118
119    try {
120      $xmlReader->setSchema($schemaFilePath);
121    } catch (Throwable $exception) {
122      $this->importLibXmlErrors($exception->getMessage() . ': ');
123      $xmlReader->close();
124      return;
125    }
126
127    do {
128      $continue = $xmlReader->read();
129      $this->importLibXmlErrors();
130    } while ($continue);
131
132    $xmlReader->close();
133  }
134
135  private function warnOnDeprecatedElements(): void {
136    foreach ($this::deprecatedElements as $deprecatedElement) {
137      foreach ($this->getXml()->xpath($deprecatedElement) as $ignored) {
138        $this->report('warning', "Element `$deprecatedElement` is deprecated.");
139      }
140    }
141  }
142
143  private function importLibXmlErrors(string $prefix = ""): void {
144    foreach (libxml_get_errors() as $error) {
145      $errorString = "{$prefix}Error [$error->code] in line $error->line";
146      $errorString .= trim($error->message);
147      $this->report('error', $errorString);
148    }
149    libxml_clear_errors();
150  }
151
152  protected function xmlGetNodeContentIfPresent(string $nodePath): string {
153    $nodes = $this->getXml()->xpath($nodePath);
154    return count($nodes) ? (string) $nodes[0] : '';
155  }
156
157  public function getRootTagName(): string { // TODO is this needed?
158
159    return $this->rootTagName;
160  }
161}