Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.17% covered (danger)
5.17%
32 / 619
1.61% covered (danger)
1.61%
1 / 62
CRAP
0.00% covered (danger)
0.00%
0 / 1
taoQtiTest_models_classes_QtiTestService
5.17% covered (danger)
5.17%
32 / 619
1.61% covered (danger)
1.61%
1 / 62
26291.71
0.00% covered (danger)
0.00%
0 / 1
 enableMetadataGuardians
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 disableMetadataGuardians
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enableMetadataValidators
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 disableMetadataValidators
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enableItemMustExist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 disableItemMustExist
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enableItemMustBeOverwritten
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 disableItemMustBeOverwritten
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getJsonTest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 checkMissingClassProperties
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 setDefaultModel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 saveJsonTest
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 fromJson
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getItems
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setItems
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 save
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getIdentifierFor
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 isIdentifierUnique
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 generateIdentifier
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 importMultipleTests
0.00% covered (danger)
0.00%
0 / 74
0.00% covered (danger)
0.00%
0 / 1
272
 clearRelatedResources
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
 importTest
0.00% covered (danger)
0.00%
0 / 221
0.00% covered (danger)
0.00%
0 / 1
1406
 deleteTestsFromClassByLabel
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 importTestDefinition
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 setQtiIndexFile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getQtiDefinitionPath
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 importTestAuxiliaryFiles
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getTestFile
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getDoc
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getDocPath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDocItems
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 setItemsToDoc
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 getQtiTestDir
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 searchInTestDirectory
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getQtiTestFile
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getRelTestPath
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 saveDoc
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 createContent
70.00% covered (warning)
70.00%
21 / 30
0.00% covered (danger)
0.00%
0 / 1
8.32
 deleteContent
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 setQtiTestFileSystem
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultDir
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 setQtiTestAcceptableLatency
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getQtiTestAcceptableLatency
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
2.02
 getQtiTestTemplateFileAsString
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadataImporter
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getMetaMetadataExtractor
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSecureResourceService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 verifyItemPermissions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
72
 deleteItemSubclassesByLabel
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getQtiPackageImportPreprocessing
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemTreeService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPsrContainer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMetaMetadataImporter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMappedProperties
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
20
 getIdentifierGenerator
33.33% covered (danger)
33.33%
1 / 3
0.00% covered (danger)
0.00%
0 / 1
3.19
 createTestIdentifier
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
4.12
 getTestLabel
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getManifestConverter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestConverter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSectionConverter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 convertAssessmentSectionRefs
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
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
21use oat\generis\model\data\event\ResourceCreated;
22use oat\oatbox\filesystem\Directory;
23use oat\oatbox\filesystem\File;
24use oat\oatbox\filesystem\FileSystemService;
25use oat\oatbox\reporting\Report;
26use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorInterface;
27use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorProxy;
28use oat\tao\model\resources\ResourceAccessDeniedException;
29use oat\tao\model\resources\SecureResourceServiceInterface;
30use oat\tao\model\TaoOntology;
31use oat\taoItems\model\Command\DeleteItemCommand;
32use oat\taoQtiItem\model\qti\converter\ManifestConverter;
33use oat\taoQtiItem\model\qti\ImportService;
34use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter;
35use oat\taoQtiItem\model\qti\metadata\importer\MetaMetadataImportMapper;
36use oat\taoQtiItem\model\qti\metadata\importer\PropertyDoesNotExistException;
37use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor;
38use oat\taoQtiItem\model\qti\metadata\MetadataGuardianResource;
39use oat\taoQtiItem\model\qti\metadata\MetadataService;
40use oat\taoQtiItem\model\qti\metadata\ontology\MappedMetadataInjector;
41use oat\taoQtiItem\model\qti\PackageParser;
42use oat\taoQtiItem\model\qti\Resource;
43use oat\taoQtiItem\model\qti\Service;
44use oat\taoQtiTest\models\cat\AdaptiveSectionInjectionException;
45use oat\taoQtiTest\models\cat\CatEngineNotFoundException;
46use oat\taoQtiTest\models\cat\CatService;
47use oat\taoQtiTest\models\classes\event\TestImportedEvent;
48use oat\taoQtiTest\models\metadata\MetadataTestContextAware;
49use oat\taoQtiTest\models\Qti\Converter\AssessmentSectionConverter;
50use oat\taoQtiTest\models\Qti\Converter\TestConverter;
51use oat\taoQtiTest\models\render\QtiPackageImportPreprocessing;
52use oat\taoQtiTest\models\test\AssessmentTestXmlFactory;
53use oat\taoTests\models\event\TestUpdatedEvent;
54use Psr\Container\ContainerInterface;
55use qtism\common\utils\Format;
56use qtism\data\AssessmentItemRef;
57use qtism\data\AssessmentSectionRef;
58use qtism\data\QtiComponentCollection;
59use qtism\data\SectionPartCollection;
60use qtism\data\storage\StorageException;
61use qtism\data\storage\xml\marshalling\UnmarshallingException;
62use qtism\data\storage\xml\XmlDocument;
63use qtism\data\storage\xml\XmlStorageException;
64use taoQtiTest_models_classes_import_TestImportForm as TestImportForm;
65use 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 */
75class 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}