* @author Jérôme Bogaerts */ class Service extends ConfigurableService { use EventManagerAwareTrait; use OntologyAwareTrait; const QTI_ITEM_FILE = 'qti.xml'; /** * Load a QTI_Item from an, RDF Item using the itemContent property of the * Item as the QTI xml * * @access public * @author Somsack Sipasseuth, * @param Resource item * @throws \common_Exception If $item is not representing an item with a QTI item model. * @return Item An item as a business object. */ public function getDataItemByRdfItem(core_kernel_classes_Resource $item, $langCode = '', $resolveXInclude = false) { $returnValue = null; try { //Parse it and build the QTI_Data_Item $file = $this->getXmlByRdfItem($item, $langCode); $qtiParser = new Parser($file); $returnValue = $qtiParser->load(); if (is_null($returnValue) && !empty($qtiParser->getErrors())) { common_Logger::w($qtiParser->displayErrors(false)); } if ($resolveXInclude && !empty($langCode)) { try { //loadxinclude $resolver = new ItemMediaResolver($item, $langCode); $xincludeLoader = new XIncludeLoader($returnValue, $resolver); $xincludeLoader->load(true); } catch (XIncludeException $exception) { common_Logger::e($exception->getMessage()); } } if (!$returnValue->getAttributeValue('xml:lang')) { $returnValue->setAttribute('xml:lang', \common_session_SessionManager::getSession()->getDataLanguage()); } } catch (FileNotFoundException $e) { // fail silently, since file might not have been created yet // $returnValue is then NULL. common_Logger::d('item(' . $item->getUri() . ') is empty, newly created?'); } catch (common_Exception $e) { common_Logger::d('item(' . $item->getUri() . ') is not existing'); } return $returnValue; } /** * Load the XML of the QTI item * * @param core_kernel_classes_Resource $item * @param string $language * @return false|string * @throws common_Exception */ public function getXmlByRdfItem(core_kernel_classes_Resource $item, $language = '') { //check if the item is QTI item if (! $this->getItemService()->hasItemModel($item, [ItemModel::MODEL_URI])) { throw new common_Exception('Non QTI item(' . $item->getUri() . ') opened via QTI Service'); } $file = $this->getItemService()->getItemDirectory($item, $language)->getFile(self::QTI_ITEM_FILE); return $file->read(); } /** * Save a QTI_Item into an RDF Item, by exporting the QTI_Item to QTI xml * and saving it in the itemContent property of the RDF Item * * @param \oat\taoQtiItem\model\qti\Item $qtiItem * @param core_kernel_classes_Resource $rdfItem * @return bool * @throws common_exception_Error * @throws common_exception_NotFound * @throws common_Exception * @throws exception\QtiModelException */ public function saveDataItemToRdfItem(Item $qtiItem, core_kernel_classes_Resource $rdfItem) { $label = mb_substr($rdfItem->getLabel(), 0, 256, 'UTF-8'); //set the current data lang in the item content to keep the integrity if ($qtiItem->hasAttribute('xml:lang') && !empty($qtiItem->getAttributeValue('xml:lang'))) { $lang = $qtiItem->getAttributeValue('xml:lang'); } else { $lang = \common_session_SessionManager::getSession()->getDataLanguage(); } $qtiItem->setAttribute('xml:lang', $lang); $qtiItem->setAttribute('label', $label); $directory = taoItems_models_classes_ItemsService::singleton()->getItemDirectory($rdfItem); $success = $directory->getFile(self::QTI_ITEM_FILE)->put($qtiItem->toXML()); if ($success) { $this->getEventManager()->trigger(new ItemUpdatedEvent($rdfItem->getUri())); } return $success; } /** * @param string|File $xml * @param core_kernel_classes_Resource $rdfItem * * @return bool * * @throws common_exception_Error * @throws common_exception_NotFound * @throws common_Exception */ public function saveXmlItemToRdfItem($xml, core_kernel_classes_Resource $rdfItem) { $sanitized = Authoring::sanitizeQtiXml($xml); $qtiItem = $this->getXmlToItemParser()->parse($sanitized); return $this->saveDataItemToRdfItem($qtiItem, $rdfItem); } /** * @param ItemCreatedEvent $event */ public function catchItemCreatedEvent(ItemCreatedEvent $event) { if ($event->getItemContent() !== null) { $this->saveXmlItemToRdfItem($event->getItemContent(), $this->getResource($event->getItemUri())); } } /** * Load a QTI item from a qti file in parameter. * * @param $file * @return null|Item * @throws Exception * @throws ParsingException */ public function loadItemFromFile($file) { $returnValue = null; if (is_string($file) && !empty($file)) { //validate the file to import try { $qtiParser = new Parser($file); $qtiParser->validate(); if (!$qtiParser->isValid()) { throw new ParsingException($qtiParser->displayErrors()); } $returnValue = $qtiParser->load(); } catch (ParsingException $pe) { throw new ParsingException($pe->getMessage()); } catch (Exception $e) { throw new Exception("Unable to load file {$file} caused by {$e->getMessage()}"); } } return $returnValue; } /** * Build the XHTML/CSS/JS from a QTI_Item to be rendered. * * @param Item $item * @param string $language * @return string */ public function renderQTIItem(Item $item, $language = 'en-US') { if (! is_null($item)) { return $item->toXHTML(['lang' => $language]); } return ''; } public function getVariableElements(Item $item) { $allData = $item->getDataForDelivery(); return $allData['variable']; } /** * Obtain a reference on the Metadata Injector/Extractor Registry. * * @return \oat\taoQtiItem\model\qti\metadata\MetadataRegistry */ public function getMetadataRegistry() { return new MetadataRegistry(); } public function hasItemModel(core_kernel_classes_Resource $item, $models) { return taoItems_models_classes_ItemsService::singleton()->hasItemModel($item, $models); } /** * Delete the contents of the item, but not the resource representing it. * * @param core_kernel_classes_Resource $item * @return bool */ public function deleteContentByRdfItem(core_kernel_classes_Resource $item) { return taoItems_models_classes_ItemsService::singleton()->deleteItemContent($item); } /** * @param core_kernel_classes_Resource $item * @return array * @throws common_exception_FileSystemError */ public function backupContentByRdfItem(core_kernel_classes_Resource $item): array { try { $itemContentProperty = $this->getItemService()->getItemContentProperty(); $oldItemContentPropertyValues = []; $newItemContentDirectoryName = tao_helpers_Uri::getUniqueId($item->getUri()) . '.' . uniqid(); $propertyLanguages = $item->getUsedLanguages($itemContentProperty); foreach ($propertyLanguages as $language) { $oldItemContentPropertyValues[$language] = (string) $item->getPropertyValuesByLg($itemContentProperty, $language)->get(0); $serial = $this->getNewSerializedItemContentDirectory($newItemContentDirectoryName, $language); $item->editPropertyValueByLg($itemContentProperty, $serial, $language); } return $oldItemContentPropertyValues; } catch (Exception $e) { $this->logError('Item content backup failed: ' . $e->getMessage()); throw new common_Exception("QTI Item backup failed. Item uri - " . $item->getUri()); } } /** * @param core_kernel_classes_Resource $item * @param array $backUpNames * @throws common_exception_FileSystemError */ public function restoreContentByRdfItem(core_kernel_classes_Resource $item, array $backUpNames): void { try { $itemContentProperty = $this->getItemService()->getItemContentProperty(); foreach ($backUpNames as $language => $itemContentPropertyValue) { $item->editPropertyValueByLg($itemContentProperty, $itemContentPropertyValue, $language); } } catch (Exception $e) { $this->logError('Rollback item error: ' . $e->getMessage()); throw new common_Exception(sprintf('Cannot rollback item. Item uri - %s :: Backup folders - %s ', $item->getUri(), json_encode($backUpNames))); } } /** * @deprecated use ServiceManager::get(\oat\taoQtiItem\model\qti\Service::class) * @return self */ public static function singleton() { return ServiceManager::getServiceManager()->get(self::class); } private function getItemService(): taoItems_models_classes_ItemsService { return $this->getServiceLocator()->get(taoItems_models_classes_ItemsService::class); } /** * @param string $newItemContentDirectoryName * @param $language * @return mixed * @throws \common_ext_ExtensionException */ private function getNewSerializedItemContentDirectory(string $newItemContentDirectoryName, $language): string { $newItemContentDirectoryPath = $this->getItemService()->composeItemDirectoryPath( $newItemContentDirectoryName, $language ); $newDirectory = $this->getItemService()->getDefaultItemDirectory()->getDirectory($newItemContentDirectoryPath); return $this->getServiceLocator()->get(FileReferenceSerializer::SERVICE_ID)->serialize($newDirectory); } private function getXmlToItemParser(): XmlToItemParser { return $this->getServiceLocator()->get(XmlToItemParser::class); } }