ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
PersonalSettingsImportAction.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
27use ILIAS\Data\Factory as DataFactory;
34use ILIAS\UI\Factory as UIFactory;
35use Psr\Http\Message\ServerRequestInterface;
36
38{
39 private const SCHEMA_FILE = __DIR__ . '/../../../xml/personal-settings-template.xsd';
40
41 public function __construct(
42 private readonly UIFactory $ui_factory,
43 private readonly Language $lng,
44 private readonly DataFactory $data_factory,
45 private readonly Filesystem $filesystem,
46 private readonly PersonalSettingsRepository $repository,
47 private readonly MainSettingsRepository $main_settings_repository,
48 private readonly ScoreSettingsRepository $score_settings_repository,
49 private readonly MarksRepository $marks_repository,
50 ) {
51 }
52
53 public function buildModal(string $url): RoundTrip
54 {
55 $input_handler = new PersonalSettingsImportHandlerGUI();
56
57 $file_upload_input = $this->ui_factory->input()->field()->file($input_handler, $this->lng->txt('import_file'))
59 ->withRequired(true)
60 ->withMaxFiles(1);
61
62 return $this->ui_factory->modal()->roundtrip(
63 $this->lng->txt('personal_settings_import'),
64 [],
65 ['upload' => $file_upload_input],
66 $url
67 )->withSubmitLabel($this->lng->txt('import'));
68 }
69
70 public function perform(ServerRequestInterface $request): void
71 {
72 $data = $this->buildModal('')->withRequest($request)->getData();
73
74 if (!isset($data['upload']) || $data['upload'] === []) {
75 throw new \InvalidArgumentException('import_file_not_valid_here');
76 }
77
78 $upload_dir = array_pop($data['upload']);
79 $files = $this->filesystem->listContents($upload_dir);
80
81 if (count($files) !== 1) {
82 throw new \InvalidArgumentException('import_file_not_valid_here');
83 }
84
85 $this->importFile($files[0]->getPath());
86
87 $this->filesystem->deleteDir($upload_dir);
88 }
89
90 public function importFile(string $file): void
91 {
92 $dom = new \DOMDocument();
93 $dom->resolveExternals = false;
94 $dom->substituteEntities = false;
95 $dom->validateOnParse = false;
96 $dom->loadXML($this->filesystem->read($file), LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
97
98 if (!$dom->schemaValidate(self::SCHEMA_FILE)) {
99 throw new \ilImportException('XML validation failed against XSD schema');
100 }
101 $doc = $dom->documentElement;
102
103 $imported_ilias_version = $this->data_factory->version($doc->getAttribute('ilias-version'));
104 $current_ilias_version = $this->data_factory->version(ILIAS_VERSION_NUMERIC);
105
106 if ($imported_ilias_version->getMajor() > $current_ilias_version->getMajor()) {
107 throw new \ilImportException('Unsupported Import between ILIAS major versions');
108 }
109
110 $main_settings_data = $this->parseElementsRecursive(
111 $this->firstChildElement($doc, 'main-settings')
112 );
113
114 $score_settings_data = $this->parseElementsRecursive(
115 $this->firstChildElement($doc, 'score-settings')
116 );
117
118 $mark_schema_data = $this->parseElementsRecursive(
119 $this->firstChildElement($doc, 'mark-schema')
120 );
121
122 $imported_template = PersonalSettingsTemplate::fromExport($this->getAttributes($doc));
123 $template = $this->repository->create(
124 $imported_template->getName(),
125 $imported_template->getDescription(),
126 $imported_template->getAuthor(),
127 $imported_template->getCreatedAt()
128 );
129
130 $this->main_settings_repository->store(
131 MainSettings::fromExport($main_settings_data)->withId($template->getSettingsId())
132 );
133 $this->score_settings_repository->store(
134 ScoreSettings::fromExport($score_settings_data)->withId($template->getSettingsId())
135 );
136
137 $mark_ids = $this->marks_repository->storeMarkSchema(MarkSchema::fromExport($mark_schema_data));
138 $this->repository->associateMarkSteps($template->getId(), $mark_ids);
139 }
140
147 private function getAttributes(\DOMElement $element): array
148 {
149 $attributes = [];
150 foreach ($element->getAttributeNames() as $name) {
151 $property_name = str_replace('-', '_', $name);
152 $attributes[$property_name] = $this->sanitizeContent($element->getAttribute($name));
153 }
154 return $attributes;
155 }
156
161 private function firstChildElement(\DOMElement $element, string $element_name): ?\DOMElement
162 {
163 foreach ($element->getElementsByTagName($element_name) as $item) {
164 if ($item instanceof \DOMElement) {
165 return $item;
166 }
167 }
168 return null;
169 }
170
175 private function parseElementsRecursive(\DOMElement $parent): mixed
176 {
177 $children = array_filter(
178 iterator_to_array($parent->childNodes),
179 static fn(mixed $child): bool => $child instanceof \DOMElement,
180 );
181
182 if ($children !== []) {
183 $settings = [];
184 foreach ($children as $child) {
185 if ($name = $child->getAttribute('name')) {
186 $settings[$name] = $this->parseElementsRecursive($child);
187 } else {
188 $settings[] = $this->parseElementsRecursive($child);
189 }
190 }
191 return $settings;
192 }
193
194 $type = $parent->getAttribute('type') ?? 'string';
195 $value = $this->sanitizeContent($parent->textContent);
196 return match($type) {
197 'string' => htmlspecialchars_decode($value),
198 'integer' => (int) $value,
199 'boolean' => $value == 'true',
200 'double' => (float) $value,
201 'NULL' => null,
202 default => throw new \InvalidArgumentException("Invalid type: {$type}"),
203 };
204 }
205
214 private function sanitizeContent(string $value): string
215 {
216 // Decode entities first so that encoded tags like &lt;script&gt; are handled
217 $decoded = html_entity_decode($value, ENT_QUOTES | ENT_HTML5, 'UTF-8');
218 // Remove all tags
219 $stripped = strip_tags($decoded);
220 // Remove non-printable control characters except for common whitespace (tab, LF, CR)
221 $clean = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', '', $stripped);
222 return trim($clean ?? '');
223 }
224}
Builds a Color from either hex- or rgb values.
Definition: Factory.php:31
Builds data types.
Definition: Factory.php:36
A class defining mark schemas for assessment test objects.
Definition: MarkSchema.php:37
static fromExport(array $data)
Creates an instance of the object from an array.
Definition: MarkSchema.php:181
static fromExport(array $data)
Creates an instance of the object from an array.
static fromExport(array $data)
Creates an instance of the object from an array.
__construct(private readonly UIFactory $ui_factory, private readonly Language $lng, private readonly DataFactory $data_factory, private readonly Filesystem $filesystem, private readonly PersonalSettingsRepository $repository, private readonly MainSettingsRepository $main_settings_repository, private readonly ScoreSettingsRepository $score_settings_repository, private readonly MarksRepository $marks_repository,)
firstChildElement(\DOMElement $element, string $element_name)
Returns the first child element of the given element with the given name.
sanitizeContent(string $value)
Sanitize string values parsed from XML to avoid displaying malicious content.
getAttributes(\DOMElement $element)
Returns the attributes of the given element as an associative array.
parseElementsRecursive(\DOMElement $parent)
Parses the given element recursively into an associative array structure.
static fromExport(array $data)
Creates an instance of the object from an array.
const ILIAS_VERSION_NUMERIC
The filesystem interface provides the public interface for the Filesystem service API consumer.
Definition: Filesystem.php:37
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
global $lng
Definition: privfeed.php:31
$url
Definition: shib_logout.php:68