Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.58% |
62 / 76 |
|
70.00% |
7 / 10 |
CRAP | |
0.00% |
0 / 1 |
XMLFile | |
81.58% |
62 / 76 |
|
70.00% |
7 / 10 |
28.91 | |
0.00% |
0 / 1 |
validate | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
4 | |||
getXML | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
readMetadata | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
readSchema | |
56.25% |
9 / 16 |
|
0.00% |
0 / 1 |
9.01 | |||
fallBackToCurrentSchemaVersion | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
validateAgainstSchema | |
62.50% |
10 / 16 |
|
0.00% |
0 / 1 |
3.47 | |||
warnOnDeprecatedElements | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
importLibXmlErrors | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
xmlGetNodeContentIfPresent | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getRootTagName | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** @noinspection PhpUnhandledExceptionInspection */ |
3 | declare(strict_types=1); |
4 | // TODO unit-tests |
5 | |
6 | class 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 | } |