Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
32.67% covered (danger)
32.67%
33 / 101
21.05% covered (danger)
21.05%
4 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Service
32.67% covered (danger)
32.67%
33 / 101
21.05% covered (danger)
21.05%
4 / 19
663.00
0.00% covered (danger)
0.00%
0 / 1
 getDataItemByRdfItem
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
90
 getXmlByRdfItem
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 saveDataItemToRdfItem
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 saveXmlItemToRdfItem
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 catchItemCreatedEvent
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 loadItemFromFile
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
 renderQTIItem
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getVariableElements
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadataRegistry
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasItemModel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deleteContentByRdfItem
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 backupContentByRdfItem
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
3
 restoreContentByRdfItem
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
3
 singleton
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 syncUniqueId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getItemService
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNewSerializedItemContentDirectory
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getXmlToItemParser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFeatureFlagChecker
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2013-2014 (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_NotFound;
26use oat\oatbox\filesystem\File;
27use oat\oatbox\filesystem\FilesystemException;
28use oat\tao\model\featureFlag\FeatureFlagChecker;
29use oat\tao\model\featureFlag\FeatureFlagCheckerInterface;
30use oat\tao\model\TaoOntology;
31use oat\taoQtiItem\model\qti\parser\XmlToItemParser;
32use tao_helpers_Uri;
33use common_exception_FileSystemError;
34use oat\generis\model\fileReference\FileReferenceSerializer;
35use oat\generis\model\OntologyAwareTrait;
36use oat\oatbox\event\EventManagerAwareTrait;
37use oat\oatbox\service\ConfigurableService;
38use oat\oatbox\service\ServiceManager;
39use oat\taoItems\model\event\ItemCreatedEvent;
40use oat\taoItems\model\event\ItemUpdatedEvent;
41use oat\taoQtiItem\helpers\Authoring;
42use oat\taoQtiItem\model\ItemModel;
43use oat\taoQtiItem\model\qti\exception\XIncludeException;
44use oat\taoQtiItem\model\qti\metadata\MetadataRegistry;
45use oat\taoQtiItem\model\qti\exception\ParsingException;
46use core_kernel_classes_Resource;
47use taoItems_models_classes_ItemsService;
48use common_Logger;
49use common_Exception;
50use Exception;
51use oat\taoItems\model\media\ItemMediaResolver;
52
53/**
54 * The QTI_Service gives you a central access to the managment methods of the
55 * objects
56 *
57 * @author Somsack Sipasseuth <sam@taotesting.com>
58 * @author Jérôme Bogaerts <jerome@taotesting.com>
59 */
60class Service extends ConfigurableService
61{
62    use EventManagerAwareTrait;
63    use OntologyAwareTrait;
64
65    public const QTI_ITEM_FILE = 'qti.xml';
66
67    /**
68     * Load a QTI_Item from an, RDF Item using the itemContent property of the
69     * Item as the QTI xml
70     *
71     * @access public
72     * @author Somsack Sipasseuth, <somsack.sipasseuth@tudor.lu>
73     * @param  Resource item
74     * @throws \common_Exception If $item is not representing an item with a QTI item model.
75     * @return Item An item as a business object.
76     */
77    public function getDataItemByRdfItem(core_kernel_classes_Resource $item, $langCode = '', $resolveXInclude = false)
78    {
79        $returnValue = null;
80
81        try {
82            //Parse it and build the QTI_Data_Item
83            $file = $this->getXmlByRdfItem($item, $langCode);
84            $qtiParser = new Parser($file);
85            $returnValue = $qtiParser->load();
86
87            if (is_null($returnValue) && !empty($qtiParser->getErrors())) {
88                common_Logger::w($qtiParser->displayErrors(false));
89            }
90
91            if ($resolveXInclude && !empty($langCode)) {
92                try {
93                    //loadxinclude
94                    $resolver = new ItemMediaResolver($item, $langCode);
95                    $xincludeLoader = new XIncludeLoader($returnValue, $resolver);
96                    $xincludeLoader->load(true);
97                } catch (XIncludeException $exception) {
98                    common_Logger::e($exception->getMessage());
99                }
100            }
101
102            if (!$returnValue->getAttributeValue('xml:lang')) {
103                $returnValue->setAttribute('xml:lang', \common_session_SessionManager::getSession()->getDataLanguage());
104            }
105        } catch (FilesystemException $e) {
106            // fail silently, since file might not have been created yet
107            // $returnValue is then NULL.
108            common_Logger::d('item(' . $item->getUri() . ') is empty, newly created?');
109        } catch (common_Exception $e) {
110            common_Logger::d('item(' . $item->getUri() . ') is not existing');
111        }
112
113        return $returnValue;
114    }
115
116    /**
117     * Load the XML of the QTI item
118     *
119     * @param core_kernel_classes_Resource $item
120     * @param string $language
121     * @return false|string
122     * @throws common_Exception
123     */
124    public function getXmlByRdfItem(core_kernel_classes_Resource $item, $language = '')
125    {
126        //check if the item is QTI item
127        if (! $this->getItemService()->hasItemModel($item, [ItemModel::MODEL_URI])) {
128            throw new common_Exception('Non QTI item(' . $item->getUri() . ') opened via QTI Service');
129        }
130
131        $file = $this->getItemService()->getItemDirectory($item, $language)->getFile(self::QTI_ITEM_FILE);
132        return $file->read();
133    }
134
135    /**
136     * Save a QTI_Item into an RDF Item, by exporting the QTI_Item to QTI xml
137     * and saving it in the itemContent property of the RDF Item
138     *
139     * @param \oat\taoQtiItem\model\qti\Item $qtiItem
140     * @param core_kernel_classes_Resource $rdfItem
141     * @return bool
142     * @throws common_exception_Error
143     * @throws common_exception_NotFound
144     * @throws common_Exception
145     * @throws exception\QtiModelException
146     */
147    public function saveDataItemToRdfItem(Item $qtiItem, core_kernel_classes_Resource $rdfItem)
148    {
149        $this->syncUniqueId($qtiItem, $rdfItem);
150
151        $label = mb_substr($rdfItem->getLabel(), 0, 256, 'UTF-8');
152        //set the current data lang in the item content to keep the integrity
153        if ($qtiItem->hasAttribute('xml:lang') && !empty($qtiItem->getAttributeValue('xml:lang'))) {
154            $lang = $qtiItem->getAttributeValue('xml:lang');
155        } else {
156            $lang = \common_session_SessionManager::getSession()->getDataLanguage();
157        }
158        $qtiItem->setAttribute('xml:lang', $lang);
159        $qtiItem->setAttribute('label', $label);
160
161        $directory = taoItems_models_classes_ItemsService::singleton()->getItemDirectory($rdfItem);
162        $success = $directory->getFile(self::QTI_ITEM_FILE)->put($qtiItem->toXML());
163
164        if ($success) {
165            $this->getEventManager()->trigger(new ItemUpdatedEvent($rdfItem->getUri()));
166        }
167
168        return $success;
169    }
170
171    /**
172     * @param string|File $xml
173     * @param core_kernel_classes_Resource $rdfItem
174     *
175     * @return bool
176     *
177     * @throws common_exception_Error
178     * @throws common_exception_NotFound
179     * @throws common_Exception
180     */
181    public function saveXmlItemToRdfItem($xml, core_kernel_classes_Resource $rdfItem)
182    {
183        $sanitized = Authoring::sanitizeQtiXml($xml);
184
185        $qtiItem = $this->getXmlToItemParser()->parse($sanitized);
186
187        return $this->saveDataItemToRdfItem($qtiItem, $rdfItem);
188    }
189
190    /**
191     * @param ItemCreatedEvent $event
192     */
193    public function catchItemCreatedEvent(ItemCreatedEvent $event)
194    {
195        if ($event->getItemContent() !== null) {
196            $this->saveXmlItemToRdfItem($event->getItemContent(), $this->getResource($event->getItemUri()));
197        }
198    }
199
200    /**
201     * Load a QTI item from a qti file in parameter.
202     *
203     * @param $file
204     * @return null|Item
205     * @throws Exception
206     * @throws ParsingException
207     */
208    public function loadItemFromFile($file)
209    {
210        $returnValue = null;
211
212        if (is_string($file) && !empty($file)) {
213            //validate the file to import
214            try {
215                $qtiParser = new Parser($file);
216                $qtiParser->validate();
217
218                if (!$qtiParser->isValid()) {
219                    throw new ParsingException($qtiParser->displayErrors());
220                }
221
222                $returnValue = $qtiParser->load();
223            } catch (ParsingException $pe) {
224                throw new ParsingException($pe->getMessage());
225            } catch (Exception $e) {
226                throw new Exception("Unable to load file {$file} caused  by {$e->getMessage()}");
227            }
228        }
229
230        return $returnValue;
231    }
232
233    /**
234     * Build the XHTML/CSS/JS from a QTI_Item to be rendered.
235     *
236     * @param Item $item
237     * @param string $language
238     * @return string
239     */
240    public function renderQTIItem(Item $item, $language = 'en-US')
241    {
242        if (! is_null($item)) {
243            return $item->toXHTML(['lang' => $language]);
244        }
245        return '';
246    }
247
248    public function getVariableElements(Item $item)
249    {
250        $allData = $item->getDataForDelivery();
251        return $allData['variable'];
252    }
253
254    /**
255     * Obtain a reference on the Metadata Injector/Extractor Registry.
256     *
257     * @return \oat\taoQtiItem\model\qti\metadata\MetadataRegistry
258     */
259    public function getMetadataRegistry()
260    {
261        return new MetadataRegistry();
262    }
263
264    public function hasItemModel(core_kernel_classes_Resource $item, $models)
265    {
266        return taoItems_models_classes_ItemsService::singleton()->hasItemModel($item, $models);
267    }
268
269    /**
270     * Delete the contents of the item, but not the resource representing it.
271     *
272     * @param core_kernel_classes_Resource $item
273     * @return bool
274     */
275    public function deleteContentByRdfItem(core_kernel_classes_Resource $item)
276    {
277        return taoItems_models_classes_ItemsService::singleton()->deleteItemContent($item);
278    }
279
280    /**
281     * @param core_kernel_classes_Resource $item
282     * @return array
283     * @throws common_exception_FileSystemError
284     */
285    public function backupContentByRdfItem(core_kernel_classes_Resource $item): array
286    {
287        try {
288            $itemContentProperty = $this->getItemService()->getItemContentProperty();
289
290            $oldItemContentPropertyValues = [];
291            $newItemContentDirectoryName = tao_helpers_Uri::getUniqueId($item->getUri()) . '.' . uniqid();
292            $propertyLanguages = $item->getUsedLanguages($itemContentProperty);
293            foreach ($propertyLanguages as $language) {
294                $oldItemContentPropertyValues[$language] = (string) $item
295                    ->getPropertyValuesByLg($itemContentProperty, $language)
296                    ->get(0);
297                $serial = $this->getNewSerializedItemContentDirectory($newItemContentDirectoryName, $language);
298
299                $item->editPropertyValueByLg($itemContentProperty, $serial, $language);
300            }
301
302            return $oldItemContentPropertyValues;
303        } catch (Exception $e) {
304            $this->logError('Item content backup failed: ' . $e->getMessage());
305            throw new common_Exception("QTI Item backup failed. Item uri - " . $item->getUri());
306        }
307    }
308
309    /**
310     * @param core_kernel_classes_Resource $item
311     * @param array $backUpNames
312     * @throws common_exception_FileSystemError
313     */
314    public function restoreContentByRdfItem(core_kernel_classes_Resource $item, array $backUpNames): void
315    {
316        try {
317            $itemContentProperty = $this->getItemService()->getItemContentProperty();
318            foreach ($backUpNames as $language => $itemContentPropertyValue) {
319                $item->editPropertyValueByLg($itemContentProperty, $itemContentPropertyValue, $language);
320            }
321        } catch (Exception $e) {
322            $this->logError('Rollback item error: ' . $e->getMessage());
323            throw new common_Exception(
324                sprintf(
325                    'Cannot rollback item. Item uri - %s :: Backup folders - %s ',
326                    $item->getUri(),
327                    json_encode($backUpNames)
328                )
329            );
330        }
331    }
332
333    /**
334     * @deprecated use ServiceManager::get(\oat\taoQtiItem\model\qti\Service::class)
335     * @return self
336     */
337    public static function singleton()
338    {
339        return ServiceManager::getServiceManager()->get(self::class);
340    }
341
342    private function syncUniqueId(Item $qtiItem, core_kernel_classes_Resource $rdfItem): void
343    {
344        if (!$this->getFeatureFlagChecker()->isEnabled('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER')) {
345            return;
346        }
347
348        $uniqueId = (string) $rdfItem->getOnePropertyValue($this->getProperty(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER));
349
350        if (!empty($uniqueId) && $uniqueId !== $qtiItem->getIdentifier()) {
351            $qtiItem->setIdentifier($uniqueId);
352        }
353    }
354
355    private function getItemService(): taoItems_models_classes_ItemsService
356    {
357        return $this->getServiceLocator()->get(taoItems_models_classes_ItemsService::class);
358    }
359
360    /**
361     * @param string $newItemContentDirectoryName
362     * @param $language
363     * @return mixed
364     * @throws \common_ext_ExtensionException
365     */
366    private function getNewSerializedItemContentDirectory(string $newItemContentDirectoryName, $language): string
367    {
368        $newItemContentDirectoryPath = $this->getItemService()->composeItemDirectoryPath(
369            $newItemContentDirectoryName,
370            $language
371        );
372        $newDirectory = $this->getItemService()->getDefaultItemDirectory()->getDirectory($newItemContentDirectoryPath);
373
374        return $this->getServiceLocator()->get(FileReferenceSerializer::SERVICE_ID)->serialize($newDirectory);
375    }
376
377    private function getXmlToItemParser(): XmlToItemParser
378    {
379        return $this->getServiceLocator()->get(XmlToItemParser::class);
380    }
381
382    private function getFeatureFlagChecker(): FeatureFlagCheckerInterface
383    {
384        return $this->getServiceLocator()->get(FeatureFlagChecker::class);
385    }
386}