Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.86% covered (danger)
1.86%
8 / 431
4.00% covered (danger)
4.00%
1 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImportService
1.86% covered (danger)
1.86%
8 / 431
4.00% covered (danger)
4.00%
1 / 25
14895.98
0.00% covered (danger)
0.00%
0 / 1
 singleton
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 importQTIFile
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 createRdfItem
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 createQtiItemModel
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 createQtiManifest
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 importQTIPACKFile
0.00% covered (danger)
0.00%
0 / 100
0.00% covered (danger)
0.00%
0 / 1
380
 checkImportLockTime
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 importQtiItem
0.00% covered (danger)
0.00%
0 / 193
0.00% covered (danger)
0.00%
0 / 1
2652
 validResponseProcessing
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getOutcomesIds
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getSetOutcomeValueIds
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getResponseProcessingRules
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 importResourceMetadata
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 rollback
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 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
 getTargetClassForAssets
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 getItemEventDispatcher
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMappedMetadataInjector
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMetaMetadataImportMapper
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUniqueNumericQtiIdentifierReplacer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 replaceUniqueNumericQtiIdentifier
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 convertToQti2
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemConverter
0.00% covered (danger)
0.00%
0 / 1
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
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) 2016-2024 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22namespace oat\taoQtiItem\model\qti;
23
24use common_exception_Error;
25use common_exception_UserReadableException;
26use common_Logger;
27use core_kernel_classes_Class;
28use core_kernel_classes_Resource;
29use DOMDocument;
30use Exception;
31use helpers_File;
32use oat\generis\model\OntologyAwareTrait;
33use oat\oatbox\reporting\Report;
34use oat\tao\model\TaoOntology;
35use oat\oatbox\mutex\LockTrait;
36use oat\taoItems\model\media\ItemMediaResolver;
37use oat\taoItems\model\media\LocalItemSource;
38use oat\taoQtiItem\helpers\Authoring;
39use oat\taoQtiItem\model\ItemModel;
40use oat\taoQtiItem\model\portableElement\exception\PortableElementException;
41use oat\taoQtiItem\model\portableElement\exception\PortableElementInvalidModelException;
42use oat\taoQtiItem\model\qti\asset\AssetManager;
43use oat\taoQtiItem\model\qti\asset\handler\LocalAssetHandler;
44use oat\taoQtiItem\model\qti\asset\handler\PortableAssetHandler;
45use oat\taoQtiItem\model\qti\asset\handler\SharedStimulusAssetHandler;
46use oat\taoQtiItem\model\qti\asset\handler\StimulusHandler;
47use oat\taoQtiItem\model\qti\converter\ItemConverter;
48use oat\taoQtiItem\model\qti\event\UpdatedItemEventDispatcher;
49use oat\taoQtiItem\model\qti\exception\ExtractException;
50use oat\taoQtiItem\model\qti\exception\ParsingException;
51use oat\taoQtiItem\model\qti\exception\TemplateException;
52use oat\taoQtiItem\model\qti\metadata\importer\MetadataImporter;
53use oat\taoQtiItem\model\qti\metadata\importer\MetaMetadataImportMapper;
54use oat\taoQtiItem\model\qti\metadata\imsManifest\MetaMetadataExtractor;
55use oat\taoQtiItem\model\qti\metadata\MetadataGuardianResource;
56use oat\taoQtiItem\model\qti\metadata\MetadataService;
57use oat\taoQtiItem\model\qti\metadata\ontology\MappedMetadataInjector;
58use oat\taoQtiItem\model\qti\parser\UniqueNumericQtiIdentifierReplacer;
59use oat\taoQtiItem\model\qti\parser\ValidationException;
60use oat\taoQtiItem\model\event\ItemImported;
61use qtism\data\QtiComponentCollection;
62use qtism\data\rules\SetOutcomeValue;
63use qtism\data\storage\xml\XmlDocument;
64use qtism\data\storage\xml\XmlStorageException;
65use qtism\runtime\processing\ResponseProcessingEngine;
66use qtism\runtime\tests\AssessmentItemSession;
67use qtism\runtime\tests\SessionManager;
68use tao_helpers_File;
69use taoItems_models_classes_ItemsService;
70use oat\oatbox\event\EventManager;
71use oat\oatbox\service\ServiceManager;
72use oat\oatbox\service\ConfigurableService;
73
74/**
75 * Short description of class oat\taoQtiItem\model\qti\ImportService
76 *
77 * @access public
78 * @author Joel Bout, <joel.bout@tudor.lu>
79 * @package taoQTI
80 */
81class ImportService extends ConfigurableService
82{
83    use OntologyAwareTrait;
84
85    use LockTrait;
86
87    public const SERVICE_ID = 'taoQtiItem/ImportService';
88
89    /**
90     * Checks that setOutcomeValue declared in the outcomeDeclaration
91     */
92    public const CONFIG_VALIDATE_RESPONSE_PROCESSING = 'validateResponseProcessing';
93
94    /**
95     * TTL of the item importing process
96     * How long item will be locked while lock service automatically release the lock
97     */
98    public const OPTION_IMPORT_LOCK_TTL = 'importLockTtl';
99
100    public const PROPERTY_QTI_ITEM_IDENTIFIER = 'http://www.tao.lu/Ontologies/TAOItem.rdf#QtiItemIdentifier';
101
102    /**
103     * @return ImportService
104     */
105    public static function singleton()
106    {
107        return ServiceManager::getServiceManager()->get(self::SERVICE_ID);
108    }
109
110    /**
111     * @var MetadataImporter Service to manage Lom metadata during package import
112     */
113    protected $metadataImporter;
114
115    /**
116     * Short description of method importQTIFile
117     *
118     * @access public
119     * @param $qtiFile
120     * @param core_kernel_classes_Class $itemClass
121     * @param bool $validate
122     * @return Report
123     * @throws \common_ext_ExtensionException
124     * @throws common_exception_Error
125     * @throws \common_Exception
126     * @author Joel Bout, <joel.bout@tudor.lu>
127     */
128    public function importQTIFile($qtiFile, core_kernel_classes_Class $itemClass, $validate = true)
129    {
130        try {
131            $qtiModel = $this->createQtiItemModel($qtiFile, $validate);
132            $rdfItem = $this->createRdfItem($itemClass, $qtiModel);
133
134            $report = Report::createSuccess(__('The IMS QTI Item was successfully imported.'), $rdfItem);
135        } catch (ValidationException $ve) {
136            $report = Report::createFailure(__('The IMS QTI Item could not be imported.'));
137            $report->add($ve->getReport());
138        }
139
140        return $report;
141    }
142
143    /**
144     *
145     * @param core_kernel_classes_Class $itemClass
146     * @param Item $qtiModel
147     * @param Resource $qtiItemResource
148     * @return core_kernel_classes_Resource
149     * @throws \common_Exception
150     * @throws common_exception_Error
151     */
152    protected function createRdfItem(core_kernel_classes_Class $itemClass, Item $qtiModel)
153    {
154        $itemService = taoItems_models_classes_ItemsService::singleton();
155        $qtiService = Service::singleton();
156
157        if (!$itemService->isItemClass($itemClass)) {
158            throw new common_exception_Error('provided non Itemclass for ' . __FUNCTION__);
159        }
160
161        $rdfItem = $itemService->createInstance($itemClass);
162
163        //set the QTI type
164        $itemService->setItemModel($rdfItem, new core_kernel_classes_Resource(ItemModel::MODEL_URI));
165
166        //set the label
167        $label = '';
168        if ($qtiModel->hasAttribute('label')) {
169            $label = $qtiModel->getAttributeValue('label');
170        }
171
172        if (empty($label)) {
173            $label = $qtiModel->getAttributeValue('title');
174        }
175        $rdfItem->setLabel($label);
176
177        //save itemcontent
178        if (!$qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem)) {
179            throw new \common_Exception('Unable to save item');
180        }
181
182
183        return $rdfItem;
184    }
185
186    protected function createQtiItemModel($qtiFile, $validate = true)
187    {
188        $qtiXml = Authoring::sanitizeQtiXml($qtiFile);
189        $qtiXml = $this->replaceUniqueNumericQtiIdentifier($qtiXml);
190        //validate the file to import
191        $qtiParser = new Parser($qtiXml);
192
193        if ($validate) {
194            $qtiParser->validate();
195
196            if (!$qtiParser->isValid()) {
197                $eStrs = [];
198                foreach ($qtiParser->getErrors() as $libXmlError) {
199                    // phpcs:disable Generic.Files.LineLength
200                    $eStrs[] = __('QTI-XML error at line %1$d "%2$s".', $libXmlError['line'], str_replace('[LibXMLError] ', '', trim($libXmlError['message'])));
201                    // phpcs:enable Generic.Files.LineLength
202                }
203
204                // Make sure there are no duplicate...
205                $eStrs = array_unique($eStrs);
206
207                // Add sub-report.
208                throw new ValidationException($qtiFile, $eStrs);
209            }
210        }
211
212        $qtiItem = $qtiParser->load();
213        if (!$qtiItem && count($qtiParser->getErrors())) {
214            $errors = [];
215            foreach ($qtiParser->getErrors() as $error) {
216                $errors[] = $error['message'];
217            }
218
219            throw new ValidationException($qtiFile, $errors);
220        }
221
222        return $qtiItem;
223    }
224
225    protected function createQtiManifest($manifestFile, $validate = true)
226    {
227        //load and validate the manifest
228        $qtiManifestParser = new ManifestParser($manifestFile);
229
230        if ($validate) {
231            $qtiManifestParser->validate();
232
233            if (!$qtiManifestParser->isValid()) {
234                $eStrs = [];
235                foreach ($qtiManifestParser->getErrors() as $libXmlError) {
236                    if (isset($libXmlError['line'])) {
237                        // phpcs:disable Generic.Files.LineLength
238                        $error = __('XML error at line %1$d "%2$s".', $libXmlError['line'], str_replace('[LibXMLError] ', '', trim($libXmlError['message'])));
239                    // phpcs:enable Generic.Files.LineLength
240                    } else {
241                        // phpcs:disable Generic.Files.LineLength
242                        $error = __('XML error "%1$s".', str_replace('[LibXMLError] ', '', trim($libXmlError['message'])));
243                        // phpcs:enable Generic.Files.LineLength
244                    }
245                    $eStrs[] = $error;
246                }
247
248                // Add sub-report.
249                throw new ValidationException($manifestFile, $eStrs);
250            }
251        }
252
253        return $qtiManifestParser->load();
254    }
255
256    /**
257     * imports a qti package and
258     * returns the number of items imported
259     *
260     * @access public
261     * @param $file
262     * @param core_kernel_classes_Class $itemClass
263     * @param bool $validate
264     * @param bool $rollbackOnError
265     * @param bool $rollbackOnWarning
266     * @param bool $enableMetadataGuardians
267     * @param bool $enableMetadataValidators
268     * @param bool $itemMustExist
269     * @param bool $itemMustBeOverwritten
270     * @return Report
271     * @throws Exception
272     * @throws ExtractException
273     * @throws ParsingException
274     * @throws \common_Exception
275     * @throws \common_ext_ExtensionException
276     * @throws common_exception_Error
277     * @author Joel Bout, <joel.bout@tudor.lu>
278     */
279    public function importQTIPACKFile(
280        $file,
281        core_kernel_classes_Class $itemClass,
282        $validate = true,
283        $rollbackOnError = false,
284        $rollbackOnWarning = false,
285        $enableMetadataGuardians = true,
286        $enableMetadataValidators = true,
287        $itemMustExist = false,
288        $itemMustBeOverwritten = false,
289        $importMetadataEnabled = false
290    ) {
291        $initialLogMsg = "Importing QTI Package with the following options:\n";
292        $initialLogMsg .= '- Rollback On Warning: ' . json_encode($rollbackOnWarning) . "\n";
293        $initialLogMsg .= '- Rollback On Error: ' . json_encode($rollbackOnError) . "\n";
294        $initialLogMsg .= '- Enable Metadata Guardians: ' . json_encode($enableMetadataGuardians) . "\n";
295        $initialLogMsg .= '- Enable Metadata Validators: ' . json_encode($enableMetadataValidators) . "\n";
296        $initialLogMsg .= '- Item Must Exist: ' . json_encode($itemMustExist) . "\n";
297        $initialLogMsg .= '- Item Must Be Overwritten: ' . json_encode($itemMustBeOverwritten) . "\n";
298        $initialLogMsg .= '- Import Metadata Enabled: ' . json_encode($importMetadataEnabled) . "\n";
299        \common_Logger::d($initialLogMsg);
300
301        //load and validate the package
302        $qtiPackageParser = new PackageParser($file);
303
304        if ($validate) {
305            $qtiPackageParser->validate();
306            if (!$qtiPackageParser->isValid()) {
307                throw new ParsingException('Invalid QTI package format');
308            }
309        }
310
311        //extract the package
312        $folder = $qtiPackageParser->extract();
313        if (!is_dir($folder)) {
314            throw new ExtractException();
315        }
316
317        $report = new Report(Report::TYPE_SUCCESS, '');
318        $successItems = [];
319        $allCreatedClasses = [];
320        $overwrittenItems = [];
321        $itemCount = 0;
322
323        try {
324            // The metadata import feature needs a DOM representation of the manifest.
325            $domManifest = new DOMDocument('1.0', 'UTF-8');
326            $domManifest->load($folder . 'imsmanifest.xml');
327
328            /** @var Resource[] $qtiItemResources */
329            $qtiItemResources = $this->createQtiManifest($folder . 'imsmanifest.xml');
330
331            if ($importMetadataEnabled) {
332                $metaMetadataValues = $this->getMetaMetadataExtractor()->extract($domManifest);
333                $mappedMetadataValues = $this->getMetaMetadataImportMapper()->mapMetaMetadataToProperties(
334                    $metaMetadataValues,
335                    $itemClass
336                );
337                $metadataValues = $this->getMetadataImporter()->extract($domManifest);
338                $notMatchingProperties = $this->checkMissingClassProperties(
339                    $metadataValues,
340                    $mappedMetadataValues['itemProperties']
341                );
342                if (!empty($notMatchingProperties)) {
343                    return Report::createError(
344                        sprintf(
345                            __('Target class is missing the following metadata properties: %s'),
346                            implode(', ', $notMatchingProperties)
347                        )
348                    );
349                }
350                if (empty($mappedMetadataValues)) {
351                    $mappedMetadataValues = $this->getMetaMetadataImportMapper()->mapMetadataToProperties(
352                        $metadataValues,
353                        $itemClass
354                    );
355                }
356            }
357
358            $sharedFiles = [];
359            $createdClasses = [];
360            foreach ($qtiItemResources as $qtiItemResource) {
361                $itemCount++;
362                $itemReport = $this->importQtiItem(
363                    $folder,
364                    $qtiItemResource,
365                    $itemClass,
366                    $sharedFiles,
367                    [],
368                    $metadataValues ?? [],
369                    $createdClasses,
370                    $enableMetadataGuardians,
371                    $enableMetadataValidators,
372                    $itemMustExist,
373                    $itemMustBeOverwritten,
374                    $overwrittenItems,
375                    isset($mappedMetadataValues['itemProperties']) ? $mappedMetadataValues['itemProperties'] : [],
376                    $importMetadataEnabled
377                );
378
379                $allCreatedClasses = array_merge($allCreatedClasses, $createdClasses);
380
381                $rdfItem = $itemReport->getData();
382
383                if ($rdfItem) {
384                    $successItems[$qtiItemResource->getIdentifier()] = $rdfItem;
385                }
386
387                $report->add($itemReport);
388            }
389        } catch (ValidationException $ve) {
390            $validationReport = Report::createFailure("The IMS Manifest file could not be validated");
391            $validationReport->add($ve->getReport());
392            $report->setMessage(__("No Items could be imported from the given IMS QTI package."));
393            $report->setType(Report::TYPE_ERROR);
394            $report->add($validationReport);
395        } catch (common_exception_UserReadableException $e) {
396            $report = new Report(Report::TYPE_ERROR, $e->getUserMessage());
397            $report->add($e);
398        }
399
400        if (!empty($successItems)) {
401            // Some items were imported from the package.
402            $report->setMessage(
403                __('%d Item(s) of %d imported from the given IMS QTI Package.', count($successItems), $itemCount)
404            );
405
406            if (count($successItems) !== $itemCount) {
407                $report->setType(Report::TYPE_WARNING);
408            }
409        } else {
410            $report->setMessage(__('No Items could be imported from the given IMS QTI package.'));
411            $report->setType(Report::TYPE_ERROR);
412        }
413
414        if ($rollbackOnError === true) {
415            if (
416                $report->getType() === Report::TYPE_ERROR || $report->contains(
417                    Report::TYPE_ERROR
418                )
419            ) {
420                $this->rollback($successItems, $report, $allCreatedClasses, $overwrittenItems);
421            }
422        } elseif ($rollbackOnWarning === true) {
423            if ($report->contains(Report::TYPE_WARNING)) {
424                $this->rollback($successItems, $report, $allCreatedClasses, $overwrittenItems);
425            }
426        }
427
428        // cleanup
429        tao_helpers_File::delTree($folder);
430
431        return $report;
432    }
433
434    /**
435     * Log events when items lock released after the configured item import ttl
436     * It is possible that somehow 2 item import processes were run at once,
437     * in this case we can get a situation when process 1 started import of the
438     * item with ID item1, then process 2 started import of the same item item1,
439     * process 2 saw that item with this ID already exists and pass information that
440     * item exists, but as we know item import is in progress and if someone try to
441     * work with items files he will see an error that files (any resources of the item)
442     * are not found and show error
443     * @param float $startImportTime
444     * @param string $itemId
445     */
446    private function checkImportLockTime(float $startImportTime, string $itemId = ''): void
447    {
448        $timeElapsedSecs = microtime(true) - $startImportTime;
449        if ($timeElapsedSecs > $this->getOption(self::OPTION_IMPORT_LOCK_TTL)) {
450            common_Logger::w('Items lock was released before item ' . $itemId . ' import finished.');
451        }
452    }
453
454    /**
455     * @param $tmpFolder
456     * @param \oat\taoQtiItem\model\qti\Resource $qtiItemResource
457     * @param $itemClass
458     * @param array $sharedFiles
459     * @param array $dependencies
460     * @param array $metadataValues
461     * @param array $createdClasses
462     * @param boolean $enableMetadataGuardians
463     * @param boolean $enableMetadataValidators
464     * @param bool $itemMustExist
465     * @param bool $itemMustBeOverwritten
466     * @param array $overwrittenItems
467     * @return Report
468     * @throws common_exception_Error
469     */
470    public function importQtiItem(
471        $tmpFolder,
472        Resource $qtiItemResource,
473        $itemClass,
474        array &$sharedFiles,
475        array $dependencies = [],
476        array $metadataValues = [],
477        &$createdClasses = [],
478        $enableMetadataGuardians = true,
479        $enableMetadataValidators = true,
480        $itemMustExist = false,
481        $itemMustBeOverwritten = false,
482        &$overwrittenItems = [],
483        $metaMedataValues = [],
484        $importMetadataEnabled = false
485    ) {
486        // if report can't be finished
487        $report = Report::createError(
488            __('IMS QTI Item referenced as "%s" cannot be imported.', $qtiItemResource->getIdentifier())
489        );
490
491        $startImportTime = microtime(true);
492
493        $lock = $this->createLock(
494            __CLASS__ . '/' . __METHOD__ . '/' . $qtiItemResource->getIdentifier(),
495            $this->getOption(self::OPTION_IMPORT_LOCK_TTL)
496        );
497        $lock->acquire(true);
498        try {
499            $qtiService = Service::singleton();
500            $overWriting = false;
501
502            //load the information about resources in the manifest
503            try {
504                $resourceIdentifier = $qtiItemResource->getIdentifier();
505                $guardian = false;
506
507                if ($enableMetadataGuardians === true) {
508                    $guardian = $this->getMetadataImporter()->guard($resourceIdentifier);
509                    if ($guardian !== false) {
510                        // Item found by guardians.
511                        if ($itemMustBeOverwritten === true) {
512                            \common_Logger::i(
513                                'Resource "' . $resourceIdentifier
514                                    . '" is already stored in the database and will be overwritten.'
515                            );
516                            $overWriting = true;
517                        } else {
518                            \common_Logger::i(
519                                'Resource "' . $resourceIdentifier
520                                    . '" is already stored in the database and will not be imported.'
521                            );
522
523                            return Report::createInfo(
524                                // phpcs:disable Generic.Files.LineLength
525                                __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was already stored in the Item Bank.', $resourceIdentifier),
526                                // phpcs:enable Generic.Files.LineLength
527                                new MetadataGuardianResource($guardian)
528                            );
529                        }
530                    } elseif ($itemMustExist === true) { // Item not found by guardians.
531                        \common_Logger::i(
532                            'Resource "' . $resourceIdentifier
533                                . '" must be already stored in the database in order to proceed.'
534                        );
535
536                        return new Report(
537                            Report::TYPE_ERROR,
538                            // phpcs:disable Generic.Files.LineLength
539                            __('The IMS QTI Item referenced as "%s" in the IMS Manifest file should have been found the Item Bank. Item not found.', $resourceIdentifier)
540                            // phpcs:enable Generic.Files.LineLength
541                        );
542                    }
543                }
544
545                if ($enableMetadataValidators === true) {
546                    $validationReport = $this->getMetadataImporter()->validate($resourceIdentifier);
547
548                    if ($validationReport->getType() !== Report::TYPE_SUCCESS) {
549                        $validationReport->setMessage(
550                            // phpcs:disable Generic.Files.LineLength
551                            __('Item metadata with identifier "%s" is not valid: ', $resourceIdentifier) . $validationReport->getMessage()
552                            // phpcs:enable Generic.Files.LineLength
553                        );
554                        \common_Logger::i('Item metadata is not valid: ' . $validationReport->getMessage());
555
556                        return $validationReport;
557                    }
558                }
559
560                $targetClass = $this->getMetadataImporter()->classLookUp($resourceIdentifier, $createdClasses);
561                $tmpQtiFile = $tmpFolder . helpers_File::urlToPath($qtiItemResource->getFile());
562                common_Logger::i('file :: ' . $qtiItemResource->getFile());
563                $this->convertToQti2($tmpQtiFile);
564                $qtiModel = $this->createQtiItemModel($tmpQtiFile);
565
566                if (
567                    $this->getOption(self::CONFIG_VALIDATE_RESPONSE_PROCESSING) && !$this->validResponseProcessing(
568                        $qtiModel
569                    )
570                ) {
571                    return Report::createError(
572                        // phpcs:disable Generic.Files.LineLength
573                        __('The IMS QTI Item referenced as "%s" in the IMS Manifest file has incorrect Response Processing and outcomeDeclaration definitions.', $resourceIdentifier)
574                        // phpcs:enable Generic.Files.LineLength
575                    );
576                }
577
578                if ($guardian !== false && $itemMustBeOverwritten) {
579                    \common_Logger::d(
580                        'Resource "' . $resourceIdentifier . '" will overwrite item with URI ' . $guardian->getUri()
581                    );
582                    $rdfItem = $guardian;
583                    $overwrittenItems[$guardian->getUri()] = $qtiService->backupContentByRdfItem($rdfItem);
584                    $qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem);
585                } else {
586                    $rdfItem = $this->createRdfItem((($targetClass !== false) ? $targetClass : $itemClass), $qtiModel);
587                }
588
589                // Setting qtiIdentifier property
590                $qtiIdentifierProperty = new \core_kernel_classes_Property(self::PROPERTY_QTI_ITEM_IDENTIFIER);
591                $rdfItem->editPropertyValues($qtiIdentifierProperty, $resourceIdentifier);
592
593                $itemAssetManager = new AssetManager();
594                $itemAssetManager->setItemContent($qtiModel->toXML());
595                $itemAssetManager->setSource($tmpFolder);
596
597                /**
598                 * Load asset handler following priority handler defined by you
599                 * The first applicable will be used to import assets
600                 */
601
602                /** Portable element handler */
603                $peHandler = new PortableAssetHandler($qtiModel, $tmpFolder, dirname($tmpQtiFile));
604                $itemAssetManager->loadAssetHandler($peHandler);
605
606                if (
607                    $this
608                        ->getServiceLocator()
609                        ->get(\common_ext_ExtensionsManager::SERVICE_ID)
610                        ->isInstalled('taoMediaManager')
611                ) {
612                    $mediaClassPath = $this->getTargetClassForAssets($itemClass, $rdfItem);
613                    /** Shared stimulus handler */
614                    $sharedStimulusHandler = new SharedStimulusAssetHandler();
615                    $sharedStimulusHandler->setServiceLocator($this->getServiceLocator());
616                    $sharedStimulusHandler
617                        ->setQtiModel($qtiModel)
618                        ->setItemSource(new ItemMediaResolver($rdfItem, ''))
619                        ->setSharedFiles($sharedFiles)
620                        ->setTargetClassPath($mediaClassPath);
621                    $itemAssetManager->loadAssetHandler($sharedStimulusHandler);
622                } else {
623                    $handler = new StimulusHandler();
624                    $handler->setQtiItem($qtiModel);
625                    $handler->setItemSource(new LocalItemSource(['item' => $rdfItem]));
626                    $itemAssetManager->loadAssetHandler($handler);
627                }
628
629                /** Local storage handler */
630                $localHandler = new LocalAssetHandler();
631                $localHandler->setItemSource(new LocalItemSource(['item' => $rdfItem]));
632                $itemAssetManager->loadAssetHandler($localHandler);
633
634                /** Copy external files to the item directory (preparation before import) */
635                $itemAssetManager->copyDependencyFiles($qtiItemResource, $dependencies);
636
637                $itemAssetManager
638                    ->importAuxiliaryFiles($qtiItemResource)
639                    ->importDependencyFiles($qtiItemResource, $dependencies);
640
641                $itemAssetManager->finalize();
642
643                if (isset($sharedStimulusHandler) && $sharedStimulusHandler instanceof SharedStimulusAssetHandler) {
644                    $sharedFiles = $sharedStimulusHandler->getSharedFiles();
645                }
646
647                $qtiModel = $this->createQtiItemModel($itemAssetManager->getItemContent(), false);
648                $qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem);
649
650                if ($importMetadataEnabled && isset($metadataValues[$resourceIdentifier])) {
651                    $this->getMappedMetadataInjector()->inject(
652                        $metaMedataValues,
653                        $metadataValues[$resourceIdentifier],
654                        $rdfItem
655                    );
656                }
657
658
659                $eventManager = ServiceManager::getServiceManager()->get(EventManager::CONFIG_ID);
660                $eventManager->trigger(new ItemImported($rdfItem, $qtiModel));
661
662                // Build report message.
663                if ($guardian !== false) {
664                    // phpcs:disable Generic.Files.LineLength
665                    $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was successfully overwritten.', $qtiItemResource->getIdentifier());
666                // phpcs:enable Generic.Files.LineLength
667                } else {
668                    // phpcs:disable Generic.Files.LineLength
669                    $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was successfully imported.', $qtiItemResource->getIdentifier());
670                    // phpcs:enable Generic.Files.LineLength
671                }
672
673                $this->getItemEventDispatcher()->dispatch($qtiModel, $rdfItem);
674
675                $report = Report::createSuccess($msg, $rdfItem);
676            } catch (ParsingException $e) {
677                $message = __('Resource "' . $resourceIdentifier . 'has an error. ') . $e->getUserMessage();
678
679                $report = new Report(Report::TYPE_ERROR, $message);
680            } catch (ValidationException $ve) {
681                $report = Report::createFailure(
682                    // phpcs:disable Generic.Files.LineLength
683                    __('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $resourceIdentifier)
684                    // phpcs:enable Generic.Files.LineLength
685                );
686                $report->add($ve->getReport());
687            } catch (XmlStorageException $e) {
688                $files = [];
689                $message = __('There are errors in the following shared stimulus : ') . PHP_EOL;
690                /** @var \LibXMLError $error */
691                foreach ($e->getErrors() as $error) {
692                    if (!in_array($error->file, $files)) {
693                        $files[] = $error->file;
694                        $message .= '- ' . basename($error->file) . ' :' . PHP_EOL;
695                    }
696                    $message .= $error->message . ' at line : ' . $error->line . PHP_EOL;
697                }
698                $message .= __(' For Resource "' . $resourceIdentifier);
699
700                $report = new Report(
701                    Report::TYPE_ERROR,
702                    $message
703                );
704            } catch (PortableElementInvalidModelException $pe) {
705                $report = Report::createFailure(
706                    // phpcs:disable Generic.Files.LineLength
707                    __('IMS QTI Item referenced as "%s" contains a portable element and cannot be imported.', $resourceIdentifier)
708                    // phpcs:enable Generic.Files.LineLength
709                );
710                $report->add($pe->getReport());
711                if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
712                    $rdfItem->delete();
713                }
714            } catch (PortableElementException $e) {
715                // an error occurred during a specific item
716                if ($e instanceof common_exception_UserReadableException) {
717                    $msg = __('Error on item %1$s : %2$s', $resourceIdentifier, $e->getUserMessage());
718                } else {
719                    $msg = __('Error on item %s', $resourceIdentifier);
720                    common_Logger::d($e->getMessage());
721                }
722                $report = new Report(Report::TYPE_ERROR, $msg);
723                if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
724                    $rdfItem->delete();
725                }
726            } catch (TemplateException $e) {
727                $report = new Report(
728                    Report::TYPE_ERROR,
729                    // phpcs:disable Generic.Files.LineLength
730                    __('The IMS QTI Item referenced as "%s" in the IMS Manifest file failed:  %s', $resourceIdentifier, $e->getMessage())
731                    // phpcs:enable Generic.Files.LineLength
732                );
733                if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
734                    $rdfItem->delete();
735                }
736            } catch (MetaMetadataException $e) {
737                $error = Reporter::createError(
738                    sprintf('Import failed at validating metametadata with message: "%s"', $e->getMessage())
739                );
740                $report->add($error);
741                common_Logger::e($e->getMessage());
742                if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
743                    $rdfItem->delete();
744                }
745            } catch (Exception $e) {
746                // an error occurred during a specific item
747                $report = new Report(
748                    Report::TYPE_ERROR,
749                    // phpcs:disable Generic.Files.LineLength
750                    __("An unknown error occured while importing the IMS QTI Package with identifier: " . $resourceIdentifier)
751                    // phpcs:enable Generic.Files.LineLength
752                );
753                if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists() && !$overWriting) {
754                    $rdfItem->delete();
755                }
756                common_Logger::e($e->getMessage());
757            }
758        } catch (ValidationException $ve) {
759            $validationReport = Report::createFailure("The IMS Manifest file could not be validated");
760            $validationReport->add($ve->getReport());
761            $report->setMessage(__("No Items could be imported from the given IMS QTI package."));
762            $report->setType(Report::TYPE_ERROR);
763            $report->add($validationReport);
764        } catch (common_exception_UserReadableException $e) {
765            $report = new Report(Report::TYPE_ERROR, __($e->getUserMessage()));
766            $report->add($e);
767        } finally {
768            $this->checkImportLockTime($startImportTime, $qtiItemResource->getIdentifier());
769            $lock->release();
770        }
771
772        return $report;
773    }
774
775    protected function validResponseProcessing(Item $qtiModel)
776    {
777        // <outcomeDeclaration> from the items qti
778        $outcomes = $this->getOutcomesIds($qtiModel);
779        // <setOutcomeValue> from the responseProcessing (template or body) also items qti
780        $rules = $this->getSetOutcomeValueIds($qtiModel);
781
782        return count(array_diff($rules, $outcomes)) === 0;
783    }
784
785    protected function getOutcomesIds(Item $qtiModel)
786    {
787        $declaredIds = [];
788        /** @var OutcomeDeclaration $outcomeDeclaration */
789        foreach ($qtiModel->getOutcomes() as $outcomeDeclaration) {
790            $declaredIds[] = $outcomeDeclaration->getIdentifier();
791        }
792
793        return $declaredIds;
794    }
795
796    protected function getSetOutcomeValueIds($qtiModel)
797    {
798        $rules = $this->getResponseProcessingRules($qtiModel);
799        $ids = [];
800        foreach ($rules as $rule) {
801            /** @var QtiComponentCollection $collection */
802            $collection = $rule->getComponentsByClassName(SetOutcomeValue::CLASS_NAME, true);
803            while ($collection->valid()) {
804                /** @var SetOutcomeValue $setOutcomeValue */
805                $setOutcomeValue = $collection->current();
806                $ids[] = $setOutcomeValue->getIdentifier();
807                $collection->next();
808            }
809        }
810
811        return array_unique($ids);
812    }
813
814    protected function getResponseProcessingRules(Item $qtiModel)
815    {
816        $rules = [];
817        $qti = $qtiModel->toQTI();
818        $qtiXmlDoc = new XmlDocument();
819        $qtiXmlDoc->loadFromString($qti);
820        $itemSession = new AssessmentItemSession($qtiXmlDoc->getDocumentComponent(), new SessionManager());
821        $responseProcessing = $itemSession->getAssessmentItem()->getResponseProcessing();
822
823        // Some items (especially to collect information) have no response processing!
824        if (
825            $responseProcessing !== null && ($responseProcessing->hasTemplate(
826            ) === true || $responseProcessing->hasTemplateLocation() === true || count(
827                $responseProcessing->getResponseRules()
828            ) > 0)
829        ) {
830            $engine = new ResponseProcessingEngine($responseProcessing, $itemSession);
831            $rules = $engine->getResponseProcessingRules();
832        }
833
834        return $rules;
835    }
836
837    /**
838     * Import metadata to a given QTI Item.
839     *
840     * @param MetadataValue[] $metadataValues An array of MetadataValue objects.
841     * @param Resource $qtiResource The object representing the QTI Resource, from an IMS Manifest perspective.
842     * @param core_kernel_classes_Resource $resource The object representing the target QTI Item in the Ontology.
843     * @param MetadataInjector[] $ontologyInjectors Implementations of MetadataInjector that will take care to inject
844     *                                              the metadata values in the appropriate Ontology Resource Properties.
845     * @throws MetadataInjectionException If an error occurs while importing the metadata.
846     * @deprecated use MetadataService::getImporter::inject()
847     *
848     */
849    public function importResourceMetadata(
850        array $metadataValues,
851        Resource $qtiResource,
852        core_kernel_classes_Resource $resource,
853        array $ontologyInjectors = []
854    ) {
855        // Filter metadata values for this given item.
856        $identifier = $qtiResource->getIdentifier();
857        if (isset($metadataValues[$identifier]) === true) {
858            \common_Logger::i("Preparing Metadata Values for resource '${identifier}'...");
859            $values = $metadataValues[$identifier];
860
861            foreach ($ontologyInjectors as $injector) {
862                $valuesCount = count($values);
863                $injectorClass = get_class($injector);
864                \common_Logger::i(
865                    "Attempting to inject ${valuesCount} Metadata Values in database for resource "
866                        . "'${identifier}' with Metadata Injector '${injectorClass}'."
867                );
868                $injector->inject($resource, [$identifier => $values]);
869            }
870        }
871    }
872
873    /**
874     * @param array $items
875     * @param Report $report
876     * @param array $createdClasses (optional)
877     * @throws common_exception_Error
878     */
879    protected function rollback(
880        array $items,
881        Report $report,
882        array $createdClasses = [],
883        array $overwrittenItems = []
884    ) {
885        $overwrittenItemsIds = array_keys($overwrittenItems);
886        $qtiService = Service::singleton();
887
888        // 1. Simply delete items that were not involved in overwriting.
889        foreach ($items as $id => $item) {
890            if (!$item instanceof MetadataGuardianResource && !in_array($item->getUri(), $overwrittenItemsIds)) {
891                \common_Logger::d("Deleting item '${id}'...");
892                @taoItems_models_classes_ItemsService::singleton()->deleteResource($item);
893
894                $report->add(
895                    new Report(
896                        Report::TYPE_WARNING,
897                        __('The IMS QTI Item referenced as "%s" in the IMS Manifest was successfully rolled back.', $id)
898                    )
899                );
900            }
901        }
902
903        // 2. Restore overwritten item contents.
904        foreach ($overwrittenItems as $overwrittenItemId => $backupName) {
905            common_Logger::d("Restoring content for item '${overwrittenItemId}'...");
906            @$qtiService->restoreContentByRdfItem(new core_kernel_classes_Resource($overwrittenItemId), $backupName);
907        }
908
909        foreach ($createdClasses as $createdClass) {
910            @$createdClass->delete();
911        }
912    }
913
914    /**
915     * Get the lom metadata importer
916     *
917     * @return MetadataImporter
918     */
919    protected function getMetadataImporter()
920    {
921        if (!$this->metadataImporter) {
922            $this->metadataImporter = $this->getServiceLocator()->get(MetadataService::SERVICE_ID)->getImporter();
923        }
924        return $this->metadataImporter;
925    }
926
927    protected function getMetaMetadataExtractor(): MetaMetadataExtractor
928    {
929        return $this->getServiceManager()->getContainer()->get(MetaMetadataExtractor::class);
930    }
931
932    /**
933     * Retrieve the labels of all parent classes up to base item class.
934     */
935    public function getTargetClassForAssets(
936        core_kernel_classes_Class $itemClass,
937        core_kernel_classes_Resource $itemResource
938    ): array {
939        // Collecting labels path from item root to the class where the item resource is stored
940        $labels = [];
941        while ($itemClass->getUri() !== TaoOntology::CLASS_URI_ITEM) {
942            $labels [] = $itemClass->getLabel();
943            $parentClasses = $itemClass->getParentClasses();
944            $itemClass = reset($parentClasses);
945        }
946
947        $path = array_reverse($labels);
948
949        // Adding item's label as the leaf class.
950        $path[] = $itemResource->getLabel();
951
952        return $path;
953    }
954
955    private function getItemEventDispatcher(): UpdatedItemEventDispatcher
956    {
957        return $this->getServiceLocator()->get(UpdatedItemEventDispatcher::class);
958    }
959
960    private function getMappedMetadataInjector(): MappedMetadataInjector
961    {
962        return $this->getServiceManager()->getContainer()->get(MappedMetadataInjector::class);
963    }
964
965    private function getMetaMetadataImportMapper(): MetaMetadataImportMapper
966    {
967        return $this->getServiceManager()->getContainer()->get(MetaMetadataImportMapper::class);
968    }
969
970    private function getUniqueNumericQtiIdentifierReplacer(): UniqueNumericQtiIdentifierReplacer
971    {
972        return $this->getServiceManager()->getContainer()->get(UniqueNumericQtiIdentifierReplacer::class);
973    }
974
975    private function replaceUniqueNumericQtiIdentifier(string $qtiXml): string
976    {
977        return $this->getUniqueNumericQtiIdentifierReplacer()->replace($qtiXml);
978    }
979
980    private function convertToQti2(string $tmpQtiFile): void
981    {
982        $this->getItemConverter()->convertToQti2($tmpQtiFile);
983    }
984
985    private function getItemConverter(): ItemConverter
986    {
987        return $this->getServiceManager()->getContainer()->get(ItemConverter::class);
988    }
989
990    /**
991     * Checks if target class has all the properties needed to import the metadata.
992     * @param array $metadataValues
993     * @param $itemProperties
994     * @return array
995     */
996    private function checkMissingClassProperties(array $metadataValues, $itemProperties): array
997    {
998        $metadataValueUris = $this->getMetadataImporter()->metadataValueUris($metadataValues);
999        return array_diff(
1000            $metadataValueUris,
1001            array_keys($itemProperties)
1002        );
1003    }
1004}