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