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