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