Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
5.19% |
32 / 616 |
|
1.61% |
1 / 62 |
CRAP | |
0.00% |
0 / 1 |
taoQtiTest_models_classes_QtiTestService | |
5.19% |
32 / 616 |
|
1.61% |
1 / 62 |
26270.91 | |
0.00% |
0 / 1 |
enableMetadataGuardians | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableMetadataGuardians | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
enableMetadataValidators | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableMetadataValidators | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
enableItemMustExist | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableItemMustExist | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
enableItemMustBeOverwritten | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
disableItemMustBeOverwritten | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getJsonTest | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
checkMissingClassProperties | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
setDefaultModel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
saveJsonTest | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
fromJson | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getItems | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setItems | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
save | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getIdentifierFor | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
isIdentifierUnique | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
generateIdentifier | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
importMultipleTests | |
0.00% |
0 / 74 |
|
0.00% |
0 / 1 |
272 | |||
clearRelatedResources | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
72 | |||
importTest | |
0.00% |
0 / 221 |
|
0.00% |
0 / 1 |
1406 | |||
deleteTestsFromClassByLabel | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
importTestDefinition | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
setQtiIndexFile | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getQtiDefinitionPath | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
importTestAuxiliaryFiles | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getTestFile | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getDoc | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getDocPath | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getDocItems | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
setItemsToDoc | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 | |||
getQtiTestDir | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
searchInTestDirectory | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getQtiTestFile | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getRelTestPath | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
saveDoc | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
createContent | |
70.00% |
21 / 30 |
|
0.00% |
0 / 1 |
8.32 | |||
deleteContent | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
setQtiTestFileSystem | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultDir | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
setQtiTestAcceptableLatency | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getQtiTestAcceptableLatency | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
getQtiTestTemplateFileAsString | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
getMetadataImporter | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getMetaMetadataExtractor | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSecureResourceService | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
verifyItemPermissions | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
72 | |||
deleteItemSubclassesByLabel | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getQtiPackageImportPreprocessing | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getItemTreeService | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTestService | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPsrContainer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMetaMetadataImporter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMappedProperties | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
getIdentifierGenerator | |
33.33% |
1 / 3 |
|
0.00% |
0 / 1 |
3.19 | |||
createTestIdentifier | |
50.00% |
4 / 8 |
|
0.00% |
0 / 1 |
4.12 | |||
getTestLabel | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getManifestConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTestConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getSectionConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
convertAssessmentSectionRefs | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | /** |
4 | * This program is free software; you can redistribute it and/or |
5 | * modify it under the terms of the GNU General Public License |
6 | * as published by the Free Software Foundation; under version 2 |
7 | * of the License (non-upgradable). |
8 | * |
9 | * This program is distributed in the hope that it will be useful, |
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 | * GNU General Public License for more details. |
13 | * |
14 | * You should have received a copy of the GNU General Public License |
15 | * along with this program; if not, write to the Free Software |
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
17 | * |
18 | * Copyright (c) 2013-2024 (original work) Open Assessment Technologies SA; |
19 | */ |
20 | |
21 | use oat\generis\model\data\event\ResourceCreated; |
22 | use oat\oatbox\filesystem\Directory; |
23 | use oat\oatbox\filesystem\File; |
24 | use oat\oatbox\filesystem\FileSystemService; |
25 | use oat\oatbox\reporting\Report; |
26 | use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorInterface; |
27 | use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorProxy; |
28 | use oat\tao\model\resources\ResourceAccessDeniedException; |
29 | use oat\tao\model\resources\SecureResourceServiceInterface; |
30 | use oat\tao\model\TaoOntology; |
31 | use oat\taoItems\model\Command\DeleteItemCommand; |
32 | use oat\taoQtiItem\model\qti\converter\ManifestConverter; |
33 | use oat\taoQtiItem\model\qti\ImportService; |
34 | use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter; |
35 | use oat\taoQtiItem\model\qti\metadata\importer\MetaMetadataImportMapper; |
36 | use oat\taoQtiItem\model\qti\metadata\importer\PropertyDoesNotExistException; |
37 | use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor; |
38 | use oat\taoQtiItem\model\qti\metadata\MetadataGuardianResource; |
39 | use oat\taoQtiItem\model\qti\metadata\MetadataService; |
40 | use oat\taoQtiItem\model\qti\metadata\ontology\MappedMetadataInjector; |
41 | use oat\taoQtiItem\model\qti\PackageParser; |
42 | use oat\taoQtiItem\model\qti\Resource; |
43 | use oat\taoQtiItem\model\qti\Service; |
44 | use oat\taoQtiTest\models\cat\AdaptiveSectionInjectionException; |
45 | use oat\taoQtiTest\models\cat\CatEngineNotFoundException; |
46 | use oat\taoQtiTest\models\cat\CatService; |
47 | use oat\taoQtiTest\models\classes\event\TestImportedEvent; |
48 | use oat\taoQtiTest\models\metadata\MetadataTestContextAware; |
49 | use oat\taoQtiTest\models\Qti\Converter\AssessmentSectionConverter; |
50 | use oat\taoQtiTest\models\Qti\Converter\TestConverter; |
51 | use oat\taoQtiTest\models\render\QtiPackageImportPreprocessing; |
52 | use oat\taoQtiTest\models\test\AssessmentTestXmlFactory; |
53 | use oat\taoTests\models\event\TestUpdatedEvent; |
54 | use Psr\Container\ContainerInterface; |
55 | use qtism\common\utils\Format; |
56 | use qtism\data\AssessmentItemRef; |
57 | use qtism\data\AssessmentSectionRef; |
58 | use qtism\data\QtiComponentCollection; |
59 | use qtism\data\SectionPartCollection; |
60 | use qtism\data\storage\StorageException; |
61 | use qtism\data\storage\xml\marshalling\UnmarshallingException; |
62 | use qtism\data\storage\xml\XmlDocument; |
63 | use qtism\data\storage\xml\XmlStorageException; |
64 | use taoQtiTest_models_classes_import_TestImportForm as TestImportForm; |
65 | use taoTests_models_classes_TestsService as TestService; |
66 | |
67 | /** |
68 | * the QTI TestModel service. |
69 | * |
70 | * @author Joel Bout <joel@taotesting.com> |
71 | * @author Bertrand Chevrier <bertrand@taotesting.com> |
72 | * @author Jerome Bogaerts <jerome@taotesting.com> |
73 | * @package taoQtiTest |
74 | */ |
75 | class taoQtiTest_models_classes_QtiTestService extends TestService |
76 | { |
77 | public const CONFIG_QTITEST_FILESYSTEM = 'qtiTestFolder'; |
78 | |
79 | public const CONFIG_QTITEST_ACCEPTABLE_LATENCY = 'qtiAcceptableLatency'; |
80 | |
81 | public const QTI_TEST_DEFINITION_INDEX = '.index/qti-test.txt'; |
82 | |
83 | public const PROPERTY_QTI_TEST_IDENTIFIER = 'http://www.tao.lu/Ontologies/TAOTest.rdf#QtiTestIdentifier'; |
84 | |
85 | public const INSTANCE_TEST_MODEL_QTI = 'http://www.tao.lu/Ontologies/TAOTest.rdf#QtiTestModel'; |
86 | |
87 | public const TAOQTITEST_FILENAME = 'tao-qtitest-testdefinition.xml'; |
88 | |
89 | public const METADATA_GUARDIAN_CONTEXT_NAME = 'tao-qtitest'; |
90 | |
91 | // phpcs:disable Generic.Files.LineLength |
92 | public const INSTANCE_FORMAL_PARAM_TEST_DEFINITION = 'http://www.tao.lu/Ontologies/TAOTest.rdf#FormalParamQtiTestDefinition'; |
93 | public const INSTANCE_FORMAL_PARAM_TEST_COMPILATION = 'http://www.tao.lu/Ontologies/TAOTest.rdf#FormalParamQtiTestCompilation'; |
94 | // phpcs:enable Generic.Files.LineLength |
95 | |
96 | public const TEST_COMPILED_FILENAME = 'compact-test'; |
97 | public const TEST_COMPILED_META_FILENAME = 'test-meta'; |
98 | public const TEST_COMPILED_METADATA_FILENAME = 'test-metadata.json'; |
99 | public const TEST_COMPILED_INDEX = 'test-index.json'; |
100 | public const TEST_COMPILED_HREF_INDEX_FILE_PREFIX = 'assessment-item-ref-href-index-'; |
101 | public const TEST_COMPILED_HREF_INDEX_FILE_EXTENSION = '.idx'; |
102 | |
103 | public const TEST_REMOTE_FOLDER = 'tao-qtitest-remote'; |
104 | public const TEST_RENDERING_STATE_NAME = 'taoQtiTestState'; |
105 | public const TEST_BASE_PATH_NAME = 'taoQtiBasePath'; |
106 | public const TEST_PLACEHOLDER_BASE_URI = 'tao://qti-directory'; |
107 | public const TEST_VIEWS_NAME = 'taoQtiViews'; |
108 | |
109 | public const XML_TEST_PART = 'testPart'; |
110 | public const XML_ASSESSMENT_SECTION = 'assessmentSection'; |
111 | public const XML_ASSESSMENT_ITEM_REF = 'assessmentItemRef'; |
112 | |
113 | private const IN_PROGRESS_LABEL = 'in progress'; |
114 | |
115 | /** |
116 | * @var MetadataImporter Service to manage Lom metadata during package import |
117 | */ |
118 | protected $metadataImporter; |
119 | |
120 | /** |
121 | * @var bool If true, it will guard and check metadata that comes from package. |
122 | */ |
123 | protected $useMetadataGuardians = true; |
124 | |
125 | /** |
126 | * @var bool If true, items contained in the test must be all found by one metadata guardian. |
127 | */ |
128 | protected $itemMustExist = false; |
129 | |
130 | /** |
131 | * @var bool If true, items found by metadata guardians will be overwritten. |
132 | */ |
133 | protected $itemMustBeOverwritten = false; |
134 | |
135 | /** |
136 | * @var bool If true, registered validators will be invoked for each test item to be imported. |
137 | */ |
138 | protected $useMetadataValidators = true; |
139 | |
140 | public function enableMetadataGuardians() |
141 | { |
142 | $this->useMetadataGuardians = true; |
143 | } |
144 | |
145 | public function disableMetadataGuardians() |
146 | { |
147 | $this->useMetadataGuardians = false; |
148 | } |
149 | |
150 | public function enableMetadataValidators() |
151 | { |
152 | $this->useMetadataValidators = true; |
153 | } |
154 | |
155 | public function disableMetadataValidators() |
156 | { |
157 | $this->useMetadataValidators = false; |
158 | } |
159 | |
160 | public function enableItemMustExist() |
161 | { |
162 | $this->itemMustExist = true; |
163 | } |
164 | |
165 | public function disableItemMustExist() |
166 | { |
167 | $this->itemMustExist = false; |
168 | } |
169 | |
170 | public function enableItemMustBeOverwritten() |
171 | { |
172 | $this->itemMustBeOverwritten = true; |
173 | } |
174 | |
175 | public function disableItemMustBeOverwritten() |
176 | { |
177 | $this->itemMustBeOverwritten = false; |
178 | } |
179 | |
180 | /** |
181 | * Get the QTI Test document formated in JSON. |
182 | * |
183 | * @param core_kernel_classes_Resource $test |
184 | * |
185 | * @return string the json |
186 | * |
187 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
188 | */ |
189 | public function getJsonTest(core_kernel_classes_Resource $test): string |
190 | { |
191 | $doc = $this->getDoc($test); |
192 | $converter = new taoQtiTest_models_classes_QtiTestConverter($doc); |
193 | |
194 | return $converter->toJson(); |
195 | } |
196 | |
197 | /** |
198 | * Checks if target class has all the properties needed to import the metadata. |
199 | * @param array $metadataValues |
200 | * @param array $mappedMetadataValues |
201 | * @return array |
202 | */ |
203 | private function checkMissingClassProperties(array $metadataValues, array $mappedMetadataValues): array |
204 | { |
205 | $metadataValueUris = $this->getMetadataImporter()->metadataValueUris($metadataValues); |
206 | return array_diff( |
207 | $metadataValueUris, |
208 | array_keys(array_merge($mappedMetadataValues['testProperties'], $mappedMetadataValues['itemProperties'])) |
209 | ); |
210 | } |
211 | |
212 | /** |
213 | * @inheritDoc |
214 | */ |
215 | protected function setDefaultModel($test): void |
216 | { |
217 | $this->setTestModel($test, $this->getResource(self::INSTANCE_TEST_MODEL_QTI)); |
218 | } |
219 | |
220 | /** |
221 | * Save the json formated test into the test resource. |
222 | * |
223 | * @param core_kernel_classes_Resource $test |
224 | * @param string $json |
225 | * |
226 | * @return bool true if saved |
227 | * |
228 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
229 | * @throws taoQtiTest_models_classes_QtiTestConverterException |
230 | */ |
231 | public function saveJsonTest(core_kernel_classes_Resource $test, $json): bool |
232 | { |
233 | $saved = false; |
234 | |
235 | if (!empty($json)) { |
236 | $this->verifyItemPermissions($test, $json); |
237 | |
238 | $doc = $this->getDoc($test); |
239 | |
240 | $converter = new taoQtiTest_models_classes_QtiTestConverter($doc); |
241 | $converter->fromJson($json); |
242 | |
243 | $saved = $this->saveDoc($test, $doc); |
244 | |
245 | $this->getEventManager()->trigger(new TestUpdatedEvent($test->getUri())); |
246 | } |
247 | return $saved; |
248 | } |
249 | |
250 | public function fromJson($json) |
251 | { |
252 | $doc = new XmlDocument('2.1'); |
253 | $converter = new taoQtiTest_models_classes_QtiTestConverter($doc); |
254 | $converter->fromJson($json); |
255 | return $doc; |
256 | } |
257 | |
258 | /** |
259 | * Get the items that are part of a given $test. |
260 | * |
261 | * @param core_kernel_classes_Resource $test A Resource describing a QTI Assessment Test. |
262 | * @return array An array of core_kernel_classes_Resource objects. The array is associative. Its keys are actually |
263 | * the assessmentItemRef identifiers. |
264 | */ |
265 | public function getItems(core_kernel_classes_Resource $test) |
266 | { |
267 | return $this->getDocItems($this->getDoc($test)); |
268 | } |
269 | |
270 | /** |
271 | * Assign items to a test and save it. |
272 | * @param core_kernel_classes_Resource $test |
273 | * @param array $items |
274 | * @return boolean true if set |
275 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
276 | */ |
277 | public function setItems(core_kernel_classes_Resource $test, array $items) |
278 | { |
279 | $doc = $this->getDoc($test); |
280 | $bound = $this->setItemsToDoc($doc, $items); |
281 | |
282 | if ($this->saveDoc($test, $doc)) { |
283 | return $bound == count($items); |
284 | } |
285 | |
286 | return false; |
287 | } |
288 | |
289 | /** |
290 | * Save the QTI test : set the items sequence and some options. |
291 | * |
292 | * @param core_kernel_classes_Resource $test A Resource describing a QTI Assessment Test. |
293 | * @param array $items the items sequence |
294 | * @param array $options the test's options |
295 | * @return boolean if nothing goes wrong |
296 | * @throws StorageException If an error occurs while serializing/unserializing QTI-XML content. |
297 | */ |
298 | public function save(core_kernel_classes_Resource $test, array $items) |
299 | { |
300 | try { |
301 | $doc = $this->getDoc($test); |
302 | $this->setItemsToDoc($doc, $items); |
303 | $saved = $this->saveDoc($test, $doc); |
304 | } catch (StorageException $e) { |
305 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
306 | "An error occured while dealing with the QTI-XML test: " . $e->getMessage(), |
307 | taoQtiTest_models_classes_QtiTestServiceException::TEST_WRITE_ERROR |
308 | ); |
309 | } |
310 | |
311 | return $saved; |
312 | } |
313 | |
314 | /** |
315 | * Get an identifier for a component of $qtiType. |
316 | * This identifier must be unique across the whole document. |
317 | * |
318 | * @param XmlDocument $doc |
319 | * @param string $qtiType the type name |
320 | * @return string the identifier |
321 | */ |
322 | public function getIdentifierFor(XmlDocument $doc, $qtiType) |
323 | { |
324 | $components = $doc->getDocumentComponent()->getIdentifiableComponents(); |
325 | $index = 1; |
326 | do { |
327 | $identifier = $this->generateIdentifier($doc, $qtiType, $index); |
328 | $index++; |
329 | } while (!$this->isIdentifierUnique($components, $identifier)); |
330 | |
331 | return $identifier; |
332 | } |
333 | |
334 | /** |
335 | * Check whether an identifier is unique against a list of components |
336 | * |
337 | * @param QtiComponentCollection $components |
338 | * @param string $identifier |
339 | * @return boolean |
340 | */ |
341 | private function isIdentifierUnique(QtiComponentCollection $components, $identifier) |
342 | { |
343 | foreach ($components as $component) { |
344 | if ($component->getIdentifier() == $identifier) { |
345 | return false; |
346 | } |
347 | } |
348 | return true; |
349 | } |
350 | |
351 | /** |
352 | * Generate an identifier from a qti type, using the syntax "qtitype-index" |
353 | * |
354 | * @param XmlDocument $doc |
355 | * @param string $qtiType |
356 | * @param int $offset |
357 | * @return string the identifier |
358 | */ |
359 | private function generateIdentifier(XmlDocument $doc, $qtiType, $offset = 1) |
360 | { |
361 | $typeList = $doc->getDocumentComponent()->getComponentsByClassName($qtiType); |
362 | return $qtiType . '-' . (count($typeList) + $offset); |
363 | } |
364 | |
365 | /** |
366 | * Import a QTI Test Package containing one or more QTI Test definitions. |
367 | * |
368 | * @param core_kernel_classes_Class $targetClass The Target RDFS class where you want the Test Resources to be |
369 | * created. |
370 | * @param string|File $file The path to the IMS archive you want to import tests from. |
371 | * @return common_report_Report An import report. |
372 | * @throws common_exception |
373 | * @throws common_exception_Error |
374 | * @throws common_exception_FileSystemError |
375 | */ |
376 | public function importMultipleTests( |
377 | core_kernel_classes_Class $targetClass, |
378 | $file, |
379 | bool $overwriteTest = false, |
380 | ?string $itemClassUri = null, |
381 | array $form = [], |
382 | ?string $overwriteTestUri = null, |
383 | ?string $packageLabel = null |
384 | ) { |
385 | $testClass = $targetClass; |
386 | $report = new Report(Report::TYPE_INFO); |
387 | $validPackage = false; |
388 | $validManifest = false; |
389 | $testsFound = false; |
390 | |
391 | $qtiPackageImportPreprocessingService = $this->getQtiPackageImportPreprocessing(); |
392 | $preprocessingReport = $qtiPackageImportPreprocessingService->run($file); |
393 | |
394 | if ($preprocessingReport) { |
395 | $report->add($preprocessingReport); |
396 | } |
397 | |
398 | // Validate the given IMS Package itself (ZIP integrity, presence of an 'imsmanifest.xml' file. |
399 | // phpcs:disable Generic.Files.LineLength |
400 | $invalidArchiveMsg = __("The provided archive is invalid. Make sure it is not corrupted and that it contains an 'imsmanifest.xml' file."); |
401 | // phpcs:enable Generic.Files.LineLength |
402 | |
403 | try { |
404 | $qtiPackageParser = new PackageParser($file); |
405 | $qtiPackageParser->validate(); |
406 | $validPackage = true; |
407 | } catch (Exception $e) { |
408 | $report->add(Report::createError($invalidArchiveMsg)); |
409 | } |
410 | |
411 | // Validate the manifest (well formed XML, valid against the schema). |
412 | if ($validPackage === true) { |
413 | $folder = $qtiPackageParser->extract(); |
414 | if (is_dir($folder) === false) { |
415 | $report->add(Report::createError($invalidArchiveMsg)); |
416 | } else { |
417 | $file = $folder . 'imsmanifest.xml'; |
418 | $qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($file); |
419 | $this->propagate($qtiManifestParser); |
420 | // For taoSetup PsrContainer is not available |
421 | // It is not required to perform manifest conversion in this process |
422 | // therefore we can skip it during taoSetup |
423 | if ($this->getPsrContainer()->has(ManifestConverter::class)) { |
424 | $this->getManifestConverter()->convertToQti2($file, $qtiManifestParser); |
425 | } |
426 | // We validate manifest file against QTI 3.0 |
427 | $qtiManifestParser->validate(); |
428 | if ($qtiManifestParser->isValid() === true) { |
429 | $validManifest = true; |
430 | |
431 | $tests = []; |
432 | foreach (Resource::getTestTypes() as $type) { |
433 | $tests = array_merge($tests, $qtiManifestParser->getResources($type)); |
434 | } |
435 | |
436 | $testsFound = (count($tests) !== 0); |
437 | |
438 | if ($testsFound !== true) { |
439 | $report->add( |
440 | Report::createError( |
441 | // phpcs:disable Generic.Files.LineLength |
442 | __("Package is valid but no tests were found. Make sure that it contains valid QTI tests.") |
443 | // phpcs:enable Generic.Files.LineLength |
444 | ) |
445 | ); |
446 | } else { |
447 | $alreadyImportedQtiResources = []; |
448 | |
449 | foreach ($tests as $qtiTestResource) { |
450 | $importTestReport = $this->importTest( |
451 | $testClass, |
452 | $qtiTestResource, |
453 | $qtiManifestParser, |
454 | $folder, |
455 | $alreadyImportedQtiResources, |
456 | $overwriteTest, |
457 | $itemClassUri, |
458 | !empty($form[TestImportForm::METADATA_FORM_ELEMENT_NAME]) ?? false, |
459 | $overwriteTestUri, |
460 | $packageLabel |
461 | ); |
462 | $report->add($importTestReport); |
463 | |
464 | if ($data = $importTestReport->getData()) { |
465 | $alreadyImportedQtiResources = array_unique( |
466 | array_merge( |
467 | $alreadyImportedQtiResources, |
468 | $data->itemQtiResources |
469 | ) |
470 | ); |
471 | } |
472 | } |
473 | } |
474 | } else { |
475 | $msg = __("The 'imsmanifest.xml' file found in the archive is not valid."); |
476 | $report->add(Report::createError($msg)); |
477 | } |
478 | |
479 | // Cleanup the folder where the archive was extracted. |
480 | tao_helpers_File::deltree($folder); |
481 | } |
482 | } |
483 | |
484 | if ($report->containsError() === true) { |
485 | $report->setMessage(__('The IMS QTI Test Package could not be imported.')); |
486 | $report->setType(Report::TYPE_ERROR); |
487 | } else { |
488 | $report->setMessage(__('IMS QTI Test Package successfully imported.')); |
489 | $report->setType(Report::TYPE_SUCCESS); |
490 | } |
491 | |
492 | if ( |
493 | $report->containsError() === true |
494 | && $validPackage === true |
495 | && $validManifest === true |
496 | && $testsFound === true |
497 | ) { |
498 | $this->clearRelatedResources($report); |
499 | } |
500 | |
501 | return $report; |
502 | } |
503 | |
504 | /** |
505 | * @param common_report_Report $report |
506 | * |
507 | * @throws common_exception_Error |
508 | * @throws common_exception_FileSystemError |
509 | */ |
510 | public function clearRelatedResources(common_report_Report $report): void |
511 | { |
512 | // We consider a test package as an atomic component, we then rollback it. |
513 | $itemService = $this->getServiceLocator()->get(taoItems_models_classes_ItemsService::class); |
514 | |
515 | foreach ($report as $r) { |
516 | $data = $r->getData(); |
517 | |
518 | // -- Rollback all items. |
519 | // 1. Simply delete items that were not involved in overwriting. |
520 | foreach ($data->newItems as $item) { |
521 | if ( |
522 | !$item instanceof MetadataGuardianResource |
523 | && !array_key_exists($item->getUri(), $data->overwrittenItems) |
524 | ) { |
525 | common_Logger::d("Rollbacking new item '" . $item->getUri() . "'..."); |
526 | @$itemService->deleteResource($item); |
527 | } |
528 | } |
529 | |
530 | // 2. Restore overwritten item contents. |
531 | foreach ($data->overwrittenItems as $overwrittenItemId => $backupName) { |
532 | common_Logger::d("Restoring content for item '{$overwrittenItemId}'..."); |
533 | @Service::singleton()->restoreContentByRdfItem( |
534 | new core_kernel_classes_Resource($overwrittenItemId), |
535 | $backupName |
536 | ); |
537 | } |
538 | |
539 | // Delete all created classes (by registered class lookups). |
540 | foreach ($data->createdClasses as $createdClass) { |
541 | @$createdClass->delete(); |
542 | } |
543 | |
544 | // Delete the target Item RDFS class. |
545 | common_Logger::t("Rollbacking Items target RDFS class '" . $data->itemClass->getLabel() . "'..."); |
546 | @$data->itemClass->delete(); |
547 | |
548 | // Delete test definition. |
549 | common_Logger::t("Rollbacking test '" . $data->rdfsResource->getLabel() . "..."); |
550 | @$this->deleteTest($data->rdfsResource); |
551 | |
552 | if (count($data->newItems) > 0) { |
553 | // phpcs:disable Generic.Files.LineLength |
554 | $msg = __("The resources related to the IMS QTI Test referenced as \"%s\" in the IMS Manifest file were rolled back.", $data->manifestResource->getIdentifier()); |
555 | // phpcs:enable Generic.Files.LineLength |
556 | $report->add(new common_report_Report(common_report_Report::TYPE_WARNING, $msg)); |
557 | } |
558 | } |
559 | } |
560 | |
561 | /** |
562 | * Import a QTI Test and its dependent Items into the TAO Platform. |
563 | * |
564 | * @param core_kernel_classes_Class $testClass The RDFS Class where Ontology resources must be created. |
565 | * @param oat\taoQtiItem\model\qti\Resource $qtiTestResource The QTI Test Resource representing the IMS QTI Test to |
566 | * be imported. |
567 | * @param taoQtiTest_models_classes_ManifestParser $manifestParser The parser used to retrieve the IMS Manifest. |
568 | * @param string $folder The absolute path to the folder where the IMS archive containing the test content |
569 | * @param oat\taoQtiItem\model\qti\Resource[] $ignoreQtiResources An array of QTI Manifest Resources to be ignored |
570 | * at import time. |
571 | * @return common_report_Report A report about how the importation behaved. |
572 | */ |
573 | protected function importTest( |
574 | core_kernel_classes_Class $testClass, |
575 | Resource $qtiTestResource, |
576 | taoQtiTest_models_classes_ManifestParser $manifestParser, |
577 | $folder, |
578 | array $ignoreQtiResources = [], |
579 | bool $overwriteTest = false, |
580 | ?string $itemClassUri = null, |
581 | bool $importMetadata = false, |
582 | ?string $overwriteTestUri = null, |
583 | ?string $packageLabel = null |
584 | ) { |
585 | /** @var ImportService $itemImportService */ |
586 | $itemImportService = $this->getServiceLocator()->get(ImportService::SERVICE_ID); |
587 | $qtiTestResourceIdentifier = $qtiTestResource->getIdentifier(); |
588 | |
589 | // The class where the items that belong to the test will be imported. |
590 | $itemParentClass = $this->getClass($itemClassUri ?: TaoOntology::CLASS_URI_ITEM); |
591 | |
592 | // Create an RDFS resource in the knowledge base that will hold |
593 | // the information about the imported QTI Test. |
594 | |
595 | $testResource = $this->createInstance($testClass, self::IN_PROGRESS_LABEL); |
596 | $qtiTestModelResource = $this->getResource(self::INSTANCE_TEST_MODEL_QTI); |
597 | $modelProperty = $this->getProperty(TestService::PROPERTY_TEST_TESTMODEL); |
598 | $testResource->editPropertyValues($modelProperty, $qtiTestModelResource); |
599 | |
600 | // Setting qtiIdentifier property |
601 | $qtiIdentifierProperty = $this->getProperty(self::PROPERTY_QTI_TEST_IDENTIFIER); |
602 | $testResource->editPropertyValues($qtiIdentifierProperty, $qtiTestResourceIdentifier); |
603 | |
604 | // Create the report that will hold information about the import |
605 | // of $qtiTestResource in TAO. |
606 | $report = new Report(Report::TYPE_INFO); |
607 | |
608 | // Load and validate the manifest |
609 | $qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($folder . 'imsmanifest.xml'); |
610 | $this->propagate($qtiManifestParser); |
611 | $qtiManifestParser->validate(); |
612 | |
613 | $domManifest = new DOMDocument('1.0', 'UTF-8'); |
614 | $domManifest->load($folder . 'imsmanifest.xml'); |
615 | |
616 | $metadataValues = $this->getMetadataImporter()->extract($domManifest); |
617 | |
618 | // Note: without this fix, metadata guardians do not work. |
619 | $this->getMetadataImporter()->setMetadataValues($metadataValues); |
620 | |
621 | // Set up $report with useful information for client code (especially for rollback). |
622 | $reportCtx = new stdClass(); |
623 | $reportCtx->manifestResource = $qtiTestResource; |
624 | $reportCtx->rdfsResource = $testResource; |
625 | $reportCtx->items = []; |
626 | $reportCtx->newItems = []; |
627 | $reportCtx->overwrittenItems = []; |
628 | $reportCtx->itemQtiResources = []; |
629 | $reportCtx->testMetadata = $metadataValues[$qtiTestResourceIdentifier] ?? []; |
630 | $reportCtx->createdClasses = []; |
631 | |
632 | // 'uriResource' key is needed by javascript in tao/views/templates/form/import.tpl |
633 | $reportCtx->uriResource = $testResource->getUri(); |
634 | |
635 | // Expected test.xml file location. |
636 | $expectedTestFile = $folder . str_replace('/', DIRECTORY_SEPARATOR, $qtiTestResource->getFile()); |
637 | |
638 | // Already imported test items (qti xml file paths). |
639 | $alreadyImportedTestItemFiles = []; |
640 | |
641 | // -- Check if the file referenced by the test QTI resource exists. |
642 | if (is_readable($expectedTestFile) === false) { |
643 | $report->add( |
644 | Report::createError( |
645 | __('No file found at location "%s".', $qtiTestResource->getFile()) |
646 | ) |
647 | ); |
648 | } else { |
649 | //Convert to QTI 2.2 |
650 | if ($this->getPsrContainer()->has(TestConverter::class)) { |
651 | $this->getTestConverter()->convertToQti2($expectedTestFile); |
652 | } |
653 | // -- Load the test in a QTISM flavour. |
654 | $testDefinition = new XmlDocument(); |
655 | |
656 | try { |
657 | $testDefinition->load($expectedTestFile, true); |
658 | $this->convertAssessmentSectionRefs( |
659 | $testDefinition->getDocumentComponent() |
660 | ->getComponentsByClassName('assessmentSectionRef'), |
661 | $folder |
662 | ); |
663 | // If any, assessmentSectionRefs will be resolved and included as part of the main test definition. |
664 | $testDefinition->includeAssessmentSectionRefs(true); |
665 | $testLabel = $packageLabel ?? $this->getTestLabel($reportCtx->testMetadata, $testDefinition); |
666 | |
667 | if ($overwriteTestUri || $overwriteTest) { |
668 | $itemsClassLabel = $testLabel; |
669 | |
670 | /** @var oat\taoQtiItem\model\qti\metadata\simple\SimpleMetadataValue $singleMetadata */ |
671 | foreach ($reportCtx->testMetadata as $singleMetadata) { |
672 | if (($singleMetadata->getPath()[1] ?? '') === RDFS_LABEL) { |
673 | $testLabel = $singleMetadata->getValue(); |
674 | } |
675 | } |
676 | |
677 | $this->deleteItemSubclassesByLabel($itemParentClass, $itemsClassLabel); |
678 | $overwriteTestUri |
679 | ? $this->getTestService()->deleteTest(new core_kernel_classes_Resource($overwriteTestUri)) |
680 | : $this->deleteTestsFromClassByLabel($testLabel, $testClass); |
681 | } |
682 | |
683 | $targetItemClass = $itemParentClass->createSubClass(self::IN_PROGRESS_LABEL); |
684 | |
685 | // add real label without saving (to not pass it separately to item importer) |
686 | $targetItemClass->label = $testLabel; |
687 | |
688 | $reportCtx->itemClass = $targetItemClass; |
689 | |
690 | $mappedProperties = $this->getMappedProperties( |
691 | $importMetadata, |
692 | $domManifest, |
693 | $reportCtx, |
694 | $testClass, |
695 | $targetItemClass |
696 | ); |
697 | |
698 | // -- Load all items related to test. |
699 | $itemError = false; |
700 | |
701 | // discover test's base path. |
702 | $dependencies = taoQtiTest_helpers_Utils::buildAssessmentItemRefsTestMap( |
703 | $testDefinition, |
704 | $manifestParser, |
705 | $folder |
706 | ); |
707 | |
708 | // Build a DOM version of the fully resolved AssessmentTest for later usage. |
709 | $transitionalDoc = new DOMDocument('1.0', 'UTF-8'); |
710 | $transitionalDoc->loadXML($testDefinition->saveToString()); |
711 | |
712 | /** @var CatService $service */ |
713 | $service = $this->getServiceLocator()->get(CatService::SERVICE_ID); |
714 | $service->importCatSectionIdsToRdfTest( |
715 | $testResource, |
716 | $testDefinition->getDocumentComponent(), |
717 | $expectedTestFile |
718 | ); |
719 | |
720 | if (count($dependencies['items']) > 0) { |
721 | // Stores shared files across multiple items to avoid duplicates. |
722 | $sharedFiles = []; |
723 | |
724 | foreach ($dependencies['items'] as $assessmentItemRefId => $qtiDependency) { |
725 | if ($qtiDependency !== false) { |
726 | if (Resource::isAssessmentItem($qtiDependency->getType())) { |
727 | $resourceIdentifier = $qtiDependency->getIdentifier(); |
728 | |
729 | if (!array_key_exists($resourceIdentifier, $ignoreQtiResources)) { |
730 | $qtiFile = $folder |
731 | . str_replace('/', DIRECTORY_SEPARATOR, $qtiDependency->getFile()); |
732 | |
733 | // If metadata should be aware of the test context... |
734 | foreach ($this->getMetadataImporter()->getExtractors() as $extractor) { |
735 | if ($extractor instanceof MetadataTestContextAware) { |
736 | $metadataValues = array_merge( |
737 | $metadataValues, |
738 | $extractor->contextualizeWithTest( |
739 | $qtiTestResource->getIdentifier(), |
740 | $transitionalDoc, |
741 | $resourceIdentifier, |
742 | $metadataValues |
743 | ) |
744 | ); |
745 | } |
746 | } |
747 | // Skip if $qtiFile already imported (multiple assessmentItemRef "hrefing" the same |
748 | // file). |
749 | if (array_key_exists($qtiFile, $alreadyImportedTestItemFiles) === false) { |
750 | $createdClasses = []; |
751 | |
752 | $itemReport = $itemImportService->importQtiItem( |
753 | $folder, |
754 | $qtiDependency, |
755 | $targetItemClass, |
756 | $sharedFiles, |
757 | $dependencies['dependencies'], |
758 | $metadataValues, |
759 | $createdClasses, |
760 | $this->useMetadataGuardians, |
761 | $this->useMetadataValidators, |
762 | $this->itemMustExist, |
763 | $this->itemMustBeOverwritten, |
764 | $reportCtx->overwrittenItems, |
765 | $mappedProperties['itemProperties'] ?? [], |
766 | $importMetadata |
767 | ); |
768 | |
769 | $reportCtx->createdClasses = array_merge( |
770 | $reportCtx->createdClasses, |
771 | $createdClasses |
772 | ); |
773 | |
774 | $rdfItem = $itemReport->getData(); |
775 | |
776 | if ($rdfItem) { |
777 | $reportCtx->items[$assessmentItemRefId] = $rdfItem; |
778 | $reportCtx->newItems[$assessmentItemRefId] = $rdfItem; |
779 | $reportCtx->itemQtiResources[$resourceIdentifier] = $rdfItem; |
780 | $alreadyImportedTestItemFiles[$qtiFile] = $rdfItem; |
781 | } else { |
782 | if (!$itemReport->getMessage()) { |
783 | $itemReport->setMessage( |
784 | // phpcs:disable Generic.Files.LineLength |
785 | __('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $resourceIdentifier) |
786 | // phpcs:enable Generic.Files.LineLength |
787 | ); |
788 | } |
789 | |
790 | $itemReport->setType(common_report_Report::TYPE_ERROR); |
791 | $itemError = ($itemError === false) ? true : $itemError; |
792 | } |
793 | |
794 | $report->add($itemReport); |
795 | } else { |
796 | // phpcs:disable Generic.Files.LineLength |
797 | $reportCtx->items[$assessmentItemRefId] = $alreadyImportedTestItemFiles[$qtiFile]; |
798 | // phpcs:enable Generic.Files.LineLength |
799 | } |
800 | } else { |
801 | // Ignored (possibily because imported in another test of the same package). |
802 | $reportCtx->items[$assessmentItemRefId] = $ignoreQtiResources[$resourceIdentifier]; |
803 | $report->add( |
804 | new common_report_Report( |
805 | common_report_Report::TYPE_SUCCESS, |
806 | // phpcs:disable Generic.Files.LineLength |
807 | __('IMS QTI Item referenced as "%s" in the IMS Manifest file successfully imported.', $resourceIdentifier) |
808 | // phpcs:enable Generic.Files.LineLength |
809 | ) |
810 | ); |
811 | } |
812 | } |
813 | } else { |
814 | // phpcs:disable Generic.Files.LineLength |
815 | $msg = __('The dependency to the IMS QTI AssessmentItemRef "%s" in the IMS Manifest file could not be resolved.', $assessmentItemRefId); |
816 | // phpcs:enable Generic.Files.LineLength |
817 | $report->add(common_report_Report::createFailure($msg)); |
818 | $itemError = ($itemError === false) ? true : $itemError; |
819 | } |
820 | } |
821 | |
822 | // If items did not produce errors, we import the test definition. |
823 | if ($itemError === false) { |
824 | common_Logger::i("Importing test with manifest identifier '{$qtiTestResourceIdentifier}'..."); |
825 | |
826 | // Second step is to take care of the test definition and the related media (auxiliary files). |
827 | |
828 | // 1. Import test definition (i.e. the QTI-XML Test file). |
829 | $testContent = $this->importTestDefinition( |
830 | $testResource, |
831 | $testDefinition, |
832 | $qtiTestResource, |
833 | $reportCtx->items, |
834 | $folder, |
835 | $report |
836 | ); |
837 | |
838 | if ($testContent !== false) { |
839 | // 2. Import test auxilliary files (e.g. stylesheets, images, ...). |
840 | $this->importTestAuxiliaryFiles($testContent, $qtiTestResource, $folder, $report); |
841 | |
842 | // 3. Give meaningful names to resources. |
843 | $testResource->setLabel($packageLabel ?? $testLabel); |
844 | $targetItemClass->setLabel($packageLabel ?? $testLabel); |
845 | |
846 | // 4. Import metadata for the resource (use same mechanics as item resources). |
847 | // Metadata will be set as property values. |
848 | //todo: fix taoSetup to be aware of containers. This is only workaround. |
849 | if ( |
850 | $this->getServiceManager()->getContainer()->has(MappedMetadataInjector::class) |
851 | && $importMetadata |
852 | ) { |
853 | $this->getServiceManager()->getContainer()->get(MappedMetadataInjector::class)->inject( |
854 | $mappedProperties['testProperties'] ?? [], |
855 | $metadataValues[$qtiTestResourceIdentifier] ?? [], |
856 | $testResource |
857 | ); |
858 | } |
859 | |
860 | |
861 | // 5. if $targetClass does not contain any instances |
862 | // (because everything resolved by class lookups), |
863 | // Just delete it. |
864 | if ($targetItemClass->countInstances() == 0) { |
865 | $targetItemClass->delete(); |
866 | } |
867 | } |
868 | } else { |
869 | $msg = __("One or more dependent IMS QTI Items could not be imported."); |
870 | $report->add(common_report_Report::createFailure($msg)); |
871 | } |
872 | } else { |
873 | // No depencies found (i.e. no item resources bound to the test). |
874 | $msg = __("No reference to any IMS QTI Item found."); |
875 | $report->add(common_report_Report::createFailure($msg)); |
876 | } |
877 | } catch (StorageException $e) { |
878 | // Source of the exception = $testDefinition->load() |
879 | // What is the reason ? |
880 | $eStrs = []; |
881 | |
882 | if (($libXmlErrors = $e->getErrors()) !== null) { |
883 | foreach ($libXmlErrors as $libXmlError) { |
884 | // phpcs:disable Generic.Files.LineLength |
885 | $eStrs[] = __('XML error at line %1$d column %2$d "%3$s".', $libXmlError->line, $libXmlError->column, trim($libXmlError->message)); |
886 | // phpcs:enable Generic.Files.LineLength |
887 | } |
888 | } |
889 | |
890 | $finalErrorString = implode("\n", $eStrs); |
891 | if (empty($finalErrorString) === true) { |
892 | common_Logger::e($e->getMessage()); |
893 | // Not XML malformation related. No info from LibXmlErrors extracted. |
894 | if (($previous = $e->getPrevious()) != null) { |
895 | // Useful information could be found here. |
896 | $finalErrorString = $previous->getMessage(); |
897 | |
898 | if ($previous instanceof UnmarshallingException) { |
899 | $domElement = $previous->getDOMElement(); |
900 | // phpcs:disable Generic.Files.LineLength |
901 | $finalErrorString = __('Inconsistency at line %1d:', $domElement->getLineNo()) . ' ' . $previous->getMessage(); |
902 | // phpcs:enable Generic.Files.LineLength |
903 | } |
904 | } elseif ($e->getMessage() !== '') { |
905 | $finalErrorString = $e->getMessage(); |
906 | } else { |
907 | $finalErrorString = __("Unknown error."); |
908 | } |
909 | } |
910 | |
911 | $msg = __("Error found in the IMS QTI Test:\n%s", $finalErrorString); |
912 | $report->add(common_report_Report::createFailure($msg)); |
913 | } catch (PropertyDoesNotExistException $e) { |
914 | $reportCtx->itemClass = $targetItemClass; |
915 | $report->add(Report::createError($e->getMessage())); |
916 | } catch (CatEngineNotFoundException $e) { |
917 | $report->add( |
918 | new common_report_Report( |
919 | common_report_Report::TYPE_ERROR, |
920 | __('No CAT Engine configured for CAT Endpoint "%s".', $e->getRequestedEndpoint()) |
921 | ) |
922 | ); |
923 | } catch (AdaptiveSectionInjectionException $e) { |
924 | $report->add( |
925 | new common_report_Report( |
926 | common_report_Report::TYPE_ERROR, |
927 | // phpcs:disable Generic.Files.LineLength |
928 | __("Items with assessmentItemRef identifiers \"%s\" are not registered in the related CAT endpoint.", implode(', ', $e->getInvalidItemIdentifiers())) |
929 | // phpcs:enable Generic.Files.LineLength |
930 | ) |
931 | ); |
932 | } |
933 | } |
934 | |
935 | $report->setData($reportCtx); |
936 | |
937 | if ($report->containsError() === false) { |
938 | $report->setType(common_report_Report::TYPE_SUCCESS); |
939 | // phpcs:disable Generic.Files.LineLength |
940 | $msg = __("IMS QTI Test referenced as \"%s\" in the IMS Manifest file successfully imported.", $qtiTestResource->getIdentifier()); |
941 | // phpcs:enable Generic.Files.LineLength |
942 | $report->setMessage($msg); |
943 | $eventManager = $this->getEventManager(); |
944 | |
945 | $eventManager->trigger(new ResourceCreated($testResource)); |
946 | $eventManager->trigger(new TestImportedEvent($testResource->getUri())); |
947 | } else { |
948 | $report->setType(common_report_Report::TYPE_ERROR); |
949 | // phpcs:disable Generic.Files.LineLength |
950 | $msg = __("The IMS QTI Test referenced as \"%s\" in the IMS Manifest file could not be imported.", $qtiTestResource->getIdentifier()); |
951 | // phpcs:enable Generic.Files.LineLength |
952 | $report->setMessage($msg); |
953 | } |
954 | |
955 | return $report; |
956 | } |
957 | |
958 | /** |
959 | * @throws common_Exception |
960 | */ |
961 | private function deleteTestsFromClassByLabel(string $testLabel, core_kernel_classes_Resource $testClass): void |
962 | { |
963 | $testService = $this->getTestService(); |
964 | |
965 | foreach ($testClass->getInstances() as $testInstance) { |
966 | if ($testInstance->getLabel() === $testLabel) { |
967 | $testService->deleteTest($testInstance); |
968 | } |
969 | } |
970 | } |
971 | |
972 | /** |
973 | * Import the Test itself by importing its QTI-XML definition into the system, after |
974 | * the QTI Items composing the test were also imported. |
975 | * |
976 | * The $itemMapping argument makes the implementation of this method able to know |
977 | * what are the items that were imported. The $itemMapping is an associative array |
978 | * where keys are the assessmentItemRef's identifiers and the values are the core_kernel_classes_Resources of |
979 | * the items that are now stored in the system. |
980 | * |
981 | * When this method returns false, it means that an error occured at the level of the content of the imported test |
982 | * itself e.g. an item referenced by the test is not present in the content package. In this case, $report might |
983 | * contain useful information to return to the client. |
984 | * |
985 | * @param core_kernel_classes_Resource $testResource A Test Resource the new content must be bind to. |
986 | * @param XmlDocument $testDefinition An XmlAssessmentTestDocument object. |
987 | * @param Resource $qtiResource The manifest resource describing the test to be imported. |
988 | * @param array $itemMapping An associative array that represents the mapping between assessmentItemRef elements |
989 | * and the imported items. |
990 | * @param string $extractionFolder The absolute path to the temporary folder containing the content of the imported |
991 | * IMS QTI Package Archive. |
992 | * @param common_report_Report $report A Report object to be filled during the import. |
993 | * @return Directory The newly created test content. |
994 | * @throws taoQtiTest_models_classes_QtiTestServiceException If an unexpected runtime error occurs. |
995 | */ |
996 | protected function importTestDefinition( |
997 | core_kernel_classes_Resource $testResource, |
998 | XmlDocument $testDefinition, |
999 | Resource $qtiResource, |
1000 | array $itemMapping, |
1001 | $extractionFolder, |
1002 | common_report_Report $report |
1003 | ) { |
1004 | |
1005 | foreach ($itemMapping as $itemRefId => $itemResource) { |
1006 | $itemRef = $testDefinition->getDocumentComponent()->getComponentByIdentifier($itemRefId); |
1007 | $itemRef->setHref($itemResource->getUri()); |
1008 | } |
1009 | |
1010 | $oldFile = $this->getQtiTestFile($testResource); |
1011 | $oldFile->delete(); |
1012 | |
1013 | $ds = DIRECTORY_SEPARATOR; |
1014 | $path = dirname($qtiResource->getFile()) . $ds . self::TAOQTITEST_FILENAME; |
1015 | $dir = $this->getQtiTestDir($testResource); |
1016 | $newFile = $dir->getFile($path); |
1017 | $newFile->write($testDefinition->saveToString()); |
1018 | $this->setQtiIndexFile($dir, $path); |
1019 | return $this->getQtiTestDir($testResource); |
1020 | } |
1021 | |
1022 | /** |
1023 | * |
1024 | * @param Directory $dir |
1025 | * @param $path |
1026 | * @return bool |
1027 | */ |
1028 | protected function setQtiIndexFile(Directory $dir, $path) |
1029 | { |
1030 | $newFile = $dir->getFile(self::QTI_TEST_DEFINITION_INDEX); |
1031 | return $newFile->put($path); |
1032 | } |
1033 | |
1034 | /** |
1035 | * @param Directory $dir |
1036 | * @return false|string |
1037 | */ |
1038 | protected function getQtiDefinitionPath(Directory $dir) |
1039 | { |
1040 | $index = $dir->getFile(self::QTI_TEST_DEFINITION_INDEX); |
1041 | if ($index->exists()) { |
1042 | return $index->read(); |
1043 | } |
1044 | return false; |
1045 | } |
1046 | |
1047 | /** |
1048 | * Imports the auxiliary files (file elements contained in the resource test element to be imported) into |
1049 | * the TAO Test Content directory. |
1050 | * |
1051 | * If some file cannot be copied, warnings will be committed. |
1052 | * |
1053 | * @param Directory $testContent The pointer to the TAO Test Content directory where auxilliary files will be |
1054 | * stored. |
1055 | * @param Resource $qtiResource The manifest resource describing the test to be imported. |
1056 | * @param string $extractionFolder The absolute path to the temporary folder containing the content of the imported |
1057 | * IMS QTI Package Archive. |
1058 | * @param common_report_Report A report about how the importation behaved. |
1059 | */ |
1060 | protected function importTestAuxiliaryFiles( |
1061 | Directory $testContent, |
1062 | Resource $qtiResource, |
1063 | $extractionFolder, |
1064 | common_report_Report $report |
1065 | ) { |
1066 | |
1067 | foreach ($qtiResource->getAuxiliaryFiles() as $aux) { |
1068 | try { |
1069 | taoQtiTest_helpers_Utils::storeQtiResource($testContent, $aux, $extractionFolder); |
1070 | } catch (common_Exception $e) { |
1071 | $report->add( |
1072 | new common_report_Report( |
1073 | common_report_Report::TYPE_WARNING, |
1074 | __('Auxiliary file not found at location "%s".', $aux) |
1075 | ) |
1076 | ); |
1077 | } |
1078 | } |
1079 | } |
1080 | |
1081 | /** |
1082 | * Get the File object corresponding to the location |
1083 | * of the test content (a directory!) on the file system. |
1084 | * |
1085 | * @param core_kernel_classes_Resource $test |
1086 | * @return null|File |
1087 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1088 | */ |
1089 | public function getTestFile(core_kernel_classes_Resource $test) |
1090 | { |
1091 | $testModel = $test->getOnePropertyValue($this->getProperty(TestService::PROPERTY_TEST_TESTMODEL)); |
1092 | if (is_null($testModel) || $testModel->getUri() != self::INSTANCE_TEST_MODEL_QTI) { |
1093 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1094 | 'The selected test is not a QTI test', |
1095 | taoQtiTest_models_classes_QtiTestServiceException::TEST_READ_ERROR |
1096 | ); |
1097 | } |
1098 | $file = $test->getOnePropertyValue($this->getProperty(TestService::PROPERTY_TEST_CONTENT)); |
1099 | |
1100 | if (!is_null($file)) { |
1101 | return $this->getFileReferenceSerializer()->unserializeFile($file->getUri()); |
1102 | } |
1103 | |
1104 | return null; |
1105 | } |
1106 | |
1107 | /** |
1108 | * Get the QTI reprensentation of a test content. |
1109 | * |
1110 | * @param core_kernel_classes_Resource $test the test to get the content from |
1111 | * @return XmlDocument the QTI representation from the test content |
1112 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1113 | */ |
1114 | public function getDoc(core_kernel_classes_Resource $test) |
1115 | { |
1116 | $doc = new XmlDocument('2.1'); |
1117 | $doc->loadFromString($this->getQtiTestFile($test)->read()); |
1118 | return $doc; |
1119 | } |
1120 | |
1121 | /** |
1122 | * Get the path of the QTI XML test definition of a given $test resource. |
1123 | * |
1124 | * @param core_kernel_classes_Resource $test |
1125 | * @throws Exception If no QTI-XML or multiple QTI-XML test definition were found. |
1126 | * @return string The absolute path to the QTI XML Test definition related to $test. |
1127 | */ |
1128 | public function getDocPath(core_kernel_classes_Resource $test) |
1129 | { |
1130 | $file = $this->getQtiTestFile($test); |
1131 | return $file->getBasename(); |
1132 | } |
1133 | |
1134 | /** |
1135 | * Get the items from a QTI test document. |
1136 | * |
1137 | * @param \qtism\data\storage\xml\XmlDocument $doc The QTI XML document to be inspected to retrieve the items. |
1138 | * @return core_kernel_classes_Resource[] An array of core_kernel_classes_Resource object indexed by |
1139 | * assessmentItemRef->identifier (string). |
1140 | */ |
1141 | private function getDocItems(XmlDocument $doc) |
1142 | { |
1143 | $itemArray = []; |
1144 | foreach ($doc->getDocumentComponent()->getComponentsByClassName('assessmentItemRef') as $itemRef) { |
1145 | $itemArray[$itemRef->getIdentifier()] = $this->getResource($itemRef->getHref()); |
1146 | } |
1147 | return $itemArray; |
1148 | } |
1149 | |
1150 | /** |
1151 | * Assign items to a QTI test. |
1152 | * @param XmlDocument $doc |
1153 | * @param array $items |
1154 | * @return int |
1155 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1156 | */ |
1157 | private function setItemsToDoc(XmlDocument $doc, array $items, $sectionIndex = 0) |
1158 | { |
1159 | |
1160 | $sections = $doc->getDocumentComponent()->getComponentsByClassName('assessmentSection'); |
1161 | if (!isset($sections[$sectionIndex])) { |
1162 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1163 | 'No section found in test at index : ' . $sectionIndex, |
1164 | taoQtiTest_models_classes_QtiTestServiceException::TEST_READ_ERROR |
1165 | ); |
1166 | } |
1167 | $section = $sections[$sectionIndex]; |
1168 | |
1169 | $itemRefs = new SectionPartCollection(); |
1170 | $itemRefIdentifiers = []; |
1171 | foreach ($items as $itemResource) { |
1172 | $itemDoc = new XmlDocument(); |
1173 | |
1174 | try { |
1175 | $itemDoc->loadFromString(Service::singleton()->getXmlByRdfItem($itemResource)); |
1176 | } catch (StorageException $e) { |
1177 | // We consider the item not compliant with QTI, let's try the next one. |
1178 | continue; |
1179 | } |
1180 | |
1181 | $itemRefIdentifier = $itemDoc->getDocumentComponent()->getIdentifier(); |
1182 | |
1183 | //enable more than one reference |
1184 | if (array_key_exists($itemRefIdentifier, $itemRefIdentifiers)) { |
1185 | $itemRefIdentifiers[$itemRefIdentifier] += 1; |
1186 | $itemRefIdentifier .= '-' . $itemRefIdentifiers[$itemRefIdentifier]; |
1187 | } else { |
1188 | $itemRefIdentifiers[$itemRefIdentifier] = 0; |
1189 | } |
1190 | $itemRefs[] = new AssessmentItemRef($itemRefIdentifier, $itemResource->getUri()); |
1191 | } |
1192 | $section->setSectionParts($itemRefs); |
1193 | |
1194 | return count($itemRefs); |
1195 | } |
1196 | |
1197 | /** |
1198 | * Get root qti test directory or crate if not exists |
1199 | * |
1200 | * @param core_kernel_classes_Resource $test |
1201 | * @param boolean $createTestFile Whether or not create an empty QTI XML test file. Default is |
1202 | * (boolean) true. |
1203 | * |
1204 | * @return Directory |
1205 | * |
1206 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1207 | * @throws common_exception_InconsistentData |
1208 | * @throws core_kernel_persistence_Exception |
1209 | */ |
1210 | public function getQtiTestDir(core_kernel_classes_Resource $test, $createTestFile = true) |
1211 | { |
1212 | $testModel = $this->getServiceLocator()->get(TestService::class)->getTestModel($test); |
1213 | |
1214 | if ($testModel->getUri() !== self::INSTANCE_TEST_MODEL_QTI) { |
1215 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1216 | 'The selected test is not a QTI test', |
1217 | taoQtiTest_models_classes_QtiTestServiceException::TEST_READ_ERROR |
1218 | ); |
1219 | } |
1220 | |
1221 | $dir = $test->getOnePropertyValue($this->getProperty(TestService::PROPERTY_TEST_CONTENT)); |
1222 | |
1223 | if (null !== $dir) { |
1224 | /** @noinspection PhpIncompatibleReturnTypeInspection */ |
1225 | return $this->getFileReferenceSerializer()->unserialize($dir); |
1226 | } |
1227 | |
1228 | return $this->createContent($test, $createTestFile); |
1229 | } |
1230 | |
1231 | protected function searchInTestDirectory(Directory $dir) |
1232 | { |
1233 | $iterator = $dir->getFlyIterator(Directory::ITERATOR_RECURSIVE | Directory::ITERATOR_FILE); |
1234 | $files = []; |
1235 | |
1236 | /** |
1237 | * @var File $file |
1238 | */ |
1239 | foreach ($iterator as $file) { |
1240 | if ($file->getBasename() === self::TAOQTITEST_FILENAME) { |
1241 | $files[] = $file; |
1242 | break; |
1243 | } |
1244 | } |
1245 | |
1246 | if (empty($files)) { |
1247 | throw new Exception('No QTI-XML test file found.'); |
1248 | } |
1249 | |
1250 | $file = current($files); |
1251 | $fileName = str_replace($dir->getPrefix() . '/', '', $file->getPrefix()); |
1252 | $this->setQtiIndexFile($dir, $fileName); |
1253 | |
1254 | return $file; |
1255 | } |
1256 | |
1257 | /** |
1258 | * Return the File containing the test definition |
1259 | * If it doesn't exist, it will be created |
1260 | * |
1261 | * @param core_kernel_classes_Resource $test |
1262 | * @throws \Exception If file is not found. |
1263 | * @return File |
1264 | */ |
1265 | public function getQtiTestFile(core_kernel_classes_Resource $test) |
1266 | { |
1267 | |
1268 | $dir = $this->getQtiTestDir($test); |
1269 | |
1270 | $file = $this->getQtiDefinitionPath($dir); |
1271 | |
1272 | if (!empty($file)) { |
1273 | return $dir->getFile($file); |
1274 | } |
1275 | return $this->searchInTestDirectory($dir); |
1276 | } |
1277 | |
1278 | /** |
1279 | * |
1280 | * @param core_kernel_classes_Resource $test |
1281 | * @throws Exception |
1282 | * @return string |
1283 | */ |
1284 | public function getRelTestPath(core_kernel_classes_Resource $test) |
1285 | { |
1286 | $testRootDir = $this->getQtiTestDir($test); |
1287 | return $testRootDir->getRelPath($this->getQtiTestFile($test)); |
1288 | } |
1289 | |
1290 | /** |
1291 | * Save the content of test from a QTI Document |
1292 | * @param core_kernel_classes_Resource $test |
1293 | * @param qtism\data\storage\xml\XmlDocument $doc |
1294 | * @return boolean true if saved |
1295 | * @throws taoQtiTest_models_classes_QtiTestServiceException |
1296 | */ |
1297 | private function saveDoc(core_kernel_classes_Resource $test, XmlDocument $doc) |
1298 | { |
1299 | $file = $this->getQtiTestFile($test); |
1300 | return $file->update($doc->saveToString()); |
1301 | } |
1302 | |
1303 | /** |
1304 | * Create the default content directory of a QTI test. |
1305 | * |
1306 | * @param core_kernel_classes_Resource $test |
1307 | * @param boolean $createTestFile Whether or not create an empty QTI XML test file. Default is (boolean) true. |
1308 | * @param boolean $preventOverride Prevent data to be overriden Default is (boolean) true. |
1309 | * |
1310 | * @return Directory the content directory |
1311 | * @throws XmlStorageException |
1312 | * @throws common_Exception |
1313 | * @throws common_exception_Error |
1314 | * @throws common_exception_InconsistentData In case of trying to override existing data. |
1315 | * @throws common_ext_ExtensionException |
1316 | * @throws taoQtiTest_models_classes_QtiTestServiceException If a runtime error occurs while creating the |
1317 | * test content. |
1318 | */ |
1319 | public function createContent(core_kernel_classes_Resource $test, $createTestFile = true, $preventOverride = true) |
1320 | { |
1321 | $dir = $this->getDefaultDir()->getDirectory(md5($test->getUri())); |
1322 | if ($dir->exists() && $preventOverride === true) { |
1323 | throw new common_exception_InconsistentData( |
1324 | 'Data directory for test ' . $test->getUri() . ' already exists.' |
1325 | ); |
1326 | } |
1327 | |
1328 | $file = $dir->getFile(self::TAOQTITEST_FILENAME); |
1329 | |
1330 | if ($createTestFile === true) { |
1331 | /** @var AssessmentTestXmlFactory $xmlBuilder */ |
1332 | $xmlBuilder = $this->getServiceLocator()->get(AssessmentTestXmlFactory::class); |
1333 | |
1334 | $testLabel = $test->getLabel(); |
1335 | $identifier = $this->createTestIdentifier($test); |
1336 | $xml = $xmlBuilder->create($identifier, $testLabel); |
1337 | |
1338 | if (!$file->write($xml)) { |
1339 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1340 | 'Unable to write raw QTI Test template.', |
1341 | taoQtiTest_models_classes_QtiTestServiceException::TEST_WRITE_ERROR |
1342 | ); |
1343 | } |
1344 | |
1345 | common_Logger::t("Created QTI Test content for test '" . $test->getUri() . "'."); |
1346 | } elseif ($file->exists()) { |
1347 | $doc = new DOMDocument('1.0', 'UTF-8'); |
1348 | $doc->loadXML($file->read()); |
1349 | |
1350 | // Label update only. |
1351 | $doc->documentElement->setAttribute('title', $test->getLabel()); |
1352 | |
1353 | if (!$file->update($doc->saveXML())) { |
1354 | $msg = 'Unable to update QTI Test file.'; |
1355 | throw new taoQtiTest_models_classes_QtiTestServiceException( |
1356 | $msg, |
1357 | taoQtiTest_models_classes_QtiTestServiceException::TEST_WRITE_ERROR |
1358 | ); |
1359 | } |
1360 | } |
1361 | |
1362 | $directory = $this->getFileReferenceSerializer()->serialize($dir); |
1363 | $test->editPropertyValues($this->getProperty(TestService::PROPERTY_TEST_CONTENT), $directory); |
1364 | |
1365 | return $dir; |
1366 | } |
1367 | |
1368 | /** |
1369 | * Delete the content of a QTI test |
1370 | * @param core_kernel_classes_Resource $test |
1371 | * @throws common_exception_Error |
1372 | */ |
1373 | public function deleteContent(core_kernel_classes_Resource $test) |
1374 | { |
1375 | $content = $test->getOnePropertyValue($this->getProperty(TestService::PROPERTY_TEST_CONTENT)); |
1376 | |
1377 | if (!is_null($content)) { |
1378 | $dir = $this->getFileReferenceSerializer()->unserialize($content); |
1379 | $dir->deleteSelf(); |
1380 | $this->getFileReferenceSerializer()->cleanUp($content); |
1381 | $test->removePropertyValue($this->getProperty(TestService::PROPERTY_TEST_CONTENT), $content); |
1382 | } |
1383 | } |
1384 | |
1385 | /** |
1386 | * Set the directory where the tests' contents are stored. |
1387 | * @param string $fsId |
1388 | */ |
1389 | public function setQtiTestFileSystem($fsId) |
1390 | { |
1391 | $ext = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest'); |
1392 | $ext->setConfig(self::CONFIG_QTITEST_FILESYSTEM, $fsId); |
1393 | } |
1394 | |
1395 | /** |
1396 | * Get the default directory where the tests' contents are stored. |
1397 | * replaces getQtiTestFileSystem |
1398 | * |
1399 | * @return Directory |
1400 | */ |
1401 | public function getDefaultDir() |
1402 | { |
1403 | $ext = $this |
1404 | ->getServiceLocator() |
1405 | ->get(common_ext_ExtensionsManager::SERVICE_ID) |
1406 | ->getExtensionById('taoQtiTest'); |
1407 | $fsId = $ext->getConfig(self::CONFIG_QTITEST_FILESYSTEM); |
1408 | return $this->getServiceLocator()->get(FileSystemService::SERVICE_ID)->getDirectory($fsId); |
1409 | } |
1410 | |
1411 | /** |
1412 | * Set the acceptable latency time (applied on qti:timeLimits->minTime, qti:timeLimits:maxTime). |
1413 | * |
1414 | * @param string $duration An ISO 8601 Duration. |
1415 | * @see http://www.php.net/manual/en/dateinterval.construct.php PHP's interval_spec format (based on ISO 8601). |
1416 | */ |
1417 | public function setQtiTestAcceptableLatency($duration) |
1418 | { |
1419 | $ext = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest'); |
1420 | $ext->setConfig(self::CONFIG_QTITEST_ACCEPTABLE_LATENCY, $duration); |
1421 | } |
1422 | |
1423 | /** |
1424 | * Get the acceptable latency time (applied on qti:timeLimits->minTime, qti:timeLimits->maxTime). |
1425 | * |
1426 | * @throws common_Exception If no value can be found as the acceptable latency in the extension's configuration |
1427 | * file. |
1428 | * @return string An ISO 8601 Duration. |
1429 | * @see http://www.php.net/manual/en/dateinterval.construct.php PHP's interval_spec format (based on ISO 8601). |
1430 | */ |
1431 | public function getQtiTestAcceptableLatency() |
1432 | { |
1433 | $ext = $this->getServiceLocator()->get(common_ext_ExtensionsManager::SERVICE_ID) |
1434 | ->getExtensionById('taoQtiTest'); |
1435 | $latency = $ext->getConfig(self::CONFIG_QTITEST_ACCEPTABLE_LATENCY); |
1436 | if (empty($latency)) { |
1437 | // Default duration for legacy code or missing config. |
1438 | return 'PT5S'; |
1439 | } |
1440 | return $latency; |
1441 | } |
1442 | |
1443 | /** |
1444 | * |
1445 | * @deprecated |
1446 | * |
1447 | * Get the content of the QTI Test template file as an XML string. |
1448 | * |
1449 | * @return string|boolean The QTI Test template file content or false if it could not be read. |
1450 | */ |
1451 | public function getQtiTestTemplateFileAsString() |
1452 | { |
1453 | $ext = $this |
1454 | ->getServiceLocator() |
1455 | ->get(common_ext_ExtensionsManager::SERVICE_ID) |
1456 | ->getExtensionById('taoQtiTest'); |
1457 | return file_get_contents( |
1458 | $ext->getDir() . 'models' . DIRECTORY_SEPARATOR . 'templates' . DIRECTORY_SEPARATOR . 'qtiTest.xml' |
1459 | ); |
1460 | } |
1461 | |
1462 | /** |
1463 | * Get the lom metadata importer |
1464 | * |
1465 | * @return MetadataImporter |
1466 | */ |
1467 | protected function getMetadataImporter() |
1468 | { |
1469 | if (!$this->metadataImporter) { |
1470 | $this->metadataImporter = $this->getServiceLocator()->get(MetadataService::SERVICE_ID)->getImporter(); |
1471 | } |
1472 | return $this->metadataImporter; |
1473 | } |
1474 | |
1475 | private function getMetaMetadataExtractor(): MetaMetadataExtractor |
1476 | { |
1477 | return $this->getPsrContainer()->get(MetaMetadataExtractor::class); |
1478 | } |
1479 | |
1480 | private function getSecureResourceService(): SecureResourceServiceInterface |
1481 | { |
1482 | return $this->getServiceLocator()->get(SecureResourceServiceInterface::SERVICE_ID); |
1483 | } |
1484 | |
1485 | /** |
1486 | * @param core_kernel_classes_Resource $oldTest |
1487 | * @param string $json |
1488 | * |
1489 | * @throws ResourceAccessDeniedException |
1490 | */ |
1491 | private function verifyItemPermissions(core_kernel_classes_Resource $oldTest, string $json): void |
1492 | { |
1493 | $array = json_decode($json, true); |
1494 | |
1495 | $ids = []; |
1496 | |
1497 | $oldItemIds = []; |
1498 | foreach ($this->getTestItems($oldTest) as $item) { |
1499 | $oldItemIds[] = $item->getUri(); |
1500 | } |
1501 | |
1502 | foreach ($array['testParts'] ?? [] as $testPart) { |
1503 | foreach ($testPart['assessmentSections'] ?? [] as $assessmentSection) { |
1504 | foreach ($assessmentSection['sectionParts'] ?? [] as $item) { |
1505 | if ( |
1506 | isset($item['href']) |
1507 | && !in_array($item['href'], $oldItemIds) |
1508 | && $item['qti-type'] ?? '' === self::XML_ASSESSMENT_ITEM_REF |
1509 | ) { |
1510 | $ids[] = $item['href']; |
1511 | } |
1512 | } |
1513 | } |
1514 | } |
1515 | |
1516 | $this->getSecureResourceService()->validatePermissions($ids, ['READ']); |
1517 | } |
1518 | |
1519 | private function deleteItemSubclassesByLabel(core_kernel_classes_Class $root, string $label): void |
1520 | { |
1521 | $itemTreeService = $this->getItemTreeService(); |
1522 | |
1523 | foreach ($root->getSubClasses() as $subClass) { |
1524 | if ($subClass->getLabel() !== $label) { |
1525 | continue; |
1526 | } |
1527 | |
1528 | foreach ($subClass->getInstances(true) as $instance) { |
1529 | $itemTreeService->delete(new DeleteItemCommand($instance, true)); |
1530 | } |
1531 | |
1532 | $itemTreeService->deleteClass($subClass); |
1533 | } |
1534 | } |
1535 | |
1536 | private function getQtiPackageImportPreprocessing(): QtiPackageImportPreprocessing |
1537 | { |
1538 | return $this->getPsrContainer()->get(QtiPackageImportPreprocessing::SERVICE_ID); |
1539 | } |
1540 | |
1541 | private function getItemTreeService(): taoItems_models_classes_ItemsService |
1542 | { |
1543 | return $this->getPsrContainer()->get(taoItems_models_classes_ItemsService::class); |
1544 | } |
1545 | |
1546 | private function getTestService(): taoTests_models_classes_TestsService |
1547 | { |
1548 | return $this->getPsrContainer()->get(taoTests_models_classes_TestsService::class); |
1549 | } |
1550 | |
1551 | private function getPsrContainer(): ContainerInterface |
1552 | { |
1553 | return $this->getServiceLocator()->getContainer(); |
1554 | } |
1555 | |
1556 | private function getMetaMetadataImporter(): MetaMetadataImportMapper |
1557 | { |
1558 | return $this->getServiceManager()->getContainer()->get(MetaMetadataImportMapper::class); |
1559 | } |
1560 | |
1561 | /** |
1562 | * @throws PropertyDoesNotExistException |
1563 | * @throws \oat\tao\model\metadata\exception\MetadataImportException |
1564 | */ |
1565 | private function getMappedProperties( |
1566 | bool $importMetadata, |
1567 | DOMDocument $domManifest, |
1568 | stdClass $reportCtx, |
1569 | core_kernel_classes_Class $testClass, |
1570 | core_kernel_classes_Class $targetItemClass |
1571 | ): array { |
1572 | if ($importMetadata === true) { |
1573 | $metaMetadataValues = $this->getMetaMetadataExtractor()->extract($domManifest); |
1574 | $reportCtx->metaMetadata = $metaMetadataValues; |
1575 | |
1576 | $mappedMetadataValues = $this->getMetaMetadataImporter() |
1577 | ->mapMetaMetadataToProperties($metaMetadataValues, $targetItemClass, $testClass); |
1578 | |
1579 | $metadataValues = $this->getMetadataImporter()->extract($domManifest); |
1580 | $notMatchingProperties = $this->checkMissingClassProperties($metadataValues, $mappedMetadataValues); |
1581 | if (!empty($notMatchingProperties)) { |
1582 | $message['checksum_result'] = false; |
1583 | $message['label'] = implode(', ', $notMatchingProperties); |
1584 | throw new PropertyDoesNotExistException($message); |
1585 | } |
1586 | if (empty($mappedMetadataValues)) { |
1587 | $mappedMetadataValues = $this->getMetaMetadataImporter()->mapMetadataToProperties( |
1588 | $metadataValues, |
1589 | $targetItemClass, |
1590 | $testClass |
1591 | ); |
1592 | } |
1593 | return $mappedMetadataValues; |
1594 | } |
1595 | |
1596 | return []; |
1597 | } |
1598 | |
1599 | private function getIdentifierGenerator(): ?IdentifierGeneratorInterface |
1600 | { |
1601 | try { |
1602 | return $this->getPsrContainer()->get(IdentifierGeneratorProxy::class); |
1603 | } catch (Throwable $exception) { |
1604 | return null; |
1605 | } |
1606 | } |
1607 | |
1608 | private function createTestIdentifier(core_kernel_classes_Resource $test): string |
1609 | { |
1610 | $generator = $this->getIdentifierGenerator(); |
1611 | $testLabel = $test->getLabel(); |
1612 | |
1613 | if ($generator) { |
1614 | return $generator->generate([IdentifierGeneratorInterface::OPTION_RESOURCE => $test]); |
1615 | } |
1616 | |
1617 | $identifier = null; |
1618 | |
1619 | if (preg_match('/^\d/', $testLabel)) { |
1620 | $identifier = 't_' . $testLabel; |
1621 | } |
1622 | |
1623 | return str_replace('_', '-', Format::sanitizeIdentifier($identifier)); |
1624 | } |
1625 | |
1626 | private function getTestLabel(array $testMetadata, XmlDocument $testDefinition): string |
1627 | { |
1628 | $labelMetadata = array_filter($testMetadata, function ($metadata) { |
1629 | return in_array(RDFS_LABEL, $metadata->getPath()); |
1630 | }); |
1631 | |
1632 | if (empty($labelMetadata)) { |
1633 | if ($testDefinition->getDocumentComponent() === null) { |
1634 | throw new Exception('No metadata label found for test and no title in the test definition.'); |
1635 | } |
1636 | |
1637 | common_Logger::w('No metadata label found for test. Using the title from the test definition.'); |
1638 | return $testDefinition->getDocumentComponent()->getTitle(); |
1639 | } |
1640 | |
1641 | if (count($labelMetadata) > 1) { |
1642 | common_Logger::w('Multiple labels found for test. Using the first one.'); |
1643 | } |
1644 | |
1645 | return reset($labelMetadata)->getValue(); |
1646 | } |
1647 | |
1648 | private function getManifestConverter(): ManifestConverter |
1649 | { |
1650 | return $this->getPsrContainer()->get(ManifestConverter::class); |
1651 | } |
1652 | |
1653 | private function getTestConverter(): TestConverter |
1654 | { |
1655 | return $this->getPsrContainer()->get(TestConverter::class); |
1656 | } |
1657 | |
1658 | private function getSectionConverter(): AssessmentSectionConverter |
1659 | { |
1660 | return $this->getPsrContainer()->get(AssessmentSectionConverter::class); |
1661 | } |
1662 | |
1663 | /** |
1664 | * @param AssessmentSectionRef[] $testDefinition |
1665 | */ |
1666 | private function convertAssessmentSectionRefs(QtiComponentCollection $assessmentSectionRefs, string $folder): void |
1667 | { |
1668 | if (!$this->getPsrContainer()->has(AssessmentSectionConverter::class)) { |
1669 | return; |
1670 | } |
1671 | |
1672 | foreach ($assessmentSectionRefs as $assessmentSectionRef) { |
1673 | $file = $folder . $assessmentSectionRef->getHref(); |
1674 | $this->getSectionConverter()->convertToQti2($file); |
1675 | } |
1676 | } |
1677 | } |