* @package taoItems */ class QtiItemCompiler extends taoItems_models_classes_ItemCompiler { /** * instance representing the service to run the QTI item * @var string */ const INSTANCE_ITEMRUNNER = 'http://www.tao.lu/Ontologies/TAOItem.rdf#ServiceQtiItemRunner'; /** * {@inheritDoc} * @see \tao_models_classes_Compiler::compile() */ public function compile() { $report = $this->internalCompile(); if ($report->getType() == common_report_Report::TYPE_SUCCESS) { // replace instances with service list($item, $publicDirectory, $privateDirectory) = $report->getData(); $report->setData($this->createQtiService($item, $publicDirectory, $privateDirectory)); } return $report; } /** * Compile qti item * * @throws taoItems_models_classes_CompilationFailedException * @return common_report_Report */ protected function internalCompile() { $item = $this->getResource(); $publicDirectory = $this->spawnPublicDirectory(); $privateDirectory = $this->spawnPrivateDirectory(); $report = new common_report_Report(common_report_Report::TYPE_SUCCESS, __('Published %s', $item->getLabel())); $report->setData([$item, $publicDirectory, $privateDirectory]); $langs = $this->getContentUsedLanguages(); if (empty($langs)) { $report->setType(common_report_Report::TYPE_ERROR); $report->setMessage(__('Item "%s" is not available in any language', $item->getLabel())); } foreach ($langs as $compilationLanguage) { $langReport = $this->deployQtiItem($item, $compilationLanguage, $publicDirectory, $privateDirectory); $report->add($langReport); if ($langReport->getType() == common_report_Report::TYPE_ERROR) { $report->setType(common_report_Report::TYPE_ERROR); $report->setMessage(__('Failed to publish %1$s in %2$s', $item->getLabel(), $compilationLanguage)); break; } } return $report; } /** * Create a servicecall that runs the prepared qti item * * @param core_kernel_classes_Resource $item * @param tao_models_classes_service_StorageDirectory $publicDirectory * @param tao_models_classes_service_StorageDirectory $privateDirectory * @return tao_models_classes_service_ServiceCall */ protected function createQtiService( core_kernel_classes_Resource $item, tao_models_classes_service_StorageDirectory $publicDirectory, tao_models_classes_service_StorageDirectory $privateDirectory ) { $service = new tao_models_classes_service_ServiceCall(new core_kernel_classes_Resource(self::INSTANCE_ITEMRUNNER)); $service->addInParameter(new tao_models_classes_service_ConstantParameter( new core_kernel_classes_Resource(taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_PATH), $publicDirectory->getId() )); $service->addInParameter( new tao_models_classes_service_ConstantParameter( new core_kernel_classes_Resource(taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_DATA_PATH), $privateDirectory->getId() ) ); $service->addInParameter( new tao_models_classes_service_ConstantParameter( new core_kernel_classes_Resource(taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_URI), $item ) ); return $service; } /** * Desploy all the required files into the provided directories * * @param core_kernel_classes_Resource $item * @param string $language * @param tao_models_classes_service_StorageDirectory $publicDirectory * @param tao_models_classes_service_StorageDirectory $privateDirectory * @return common_report_Report */ protected function deployQtiItem( core_kernel_classes_Resource $item, $language, tao_models_classes_service_StorageDirectory $publicDirectory, tao_models_classes_service_StorageDirectory $privateDirectory ) { $itemService = taoItems_models_classes_ItemsService::singleton(); $qtiService = Service::singleton(); //copy item.xml file to private directory $itemDir = $itemService->getItemDirectory($item, $language); $sourceItem = $itemDir->getFile('qti.xml'); $privateDirectory->writeStream($language . '/qti.xml', $sourceItem->readStream()); //copy client side resources (javascript loader) $qtiItemDir = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiItem')->getDir(); $taoDir = \common_ext_ExtensionsManager::singleton()->getExtensionById('tao')->getDir(); $assetPath = $qtiItemDir . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR; $assetLibPath = $taoDir . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR; if (\tao_helpers_Mode::is('production')) { $fh = fopen($assetPath . 'loader' . DIRECTORY_SEPARATOR . 'qtiLoader.min.js', 'r'); $publicDirectory->writeStream($language . '/qtiLoader.min.js', $fh); fclose($fh); } else { $fh = fopen($assetPath . 'runtime' . DIRECTORY_SEPARATOR . 'qtiLoader.js', 'r'); $publicDirectory->writeStream($language . '/qtiLoader.js', $fh); fclose($fh); $fh = fopen($assetLibPath . 'require.js', 'r'); $publicDirectory->writeStream($language . '/require.js', $fh); fclose($fh); } // retrieve the media assets try { $qtiItem = $this->retrieveAssets($item, $language, $publicDirectory); $this->compileItemIndex($item->getUri(), $qtiItem, $language); //store variable qti elements data into the private directory $variableElements = $qtiService->getVariableElements($qtiItem); $stream = \GuzzleHttp\Psr7\stream_for(json_encode($variableElements)); $privateDirectory->writePsrStream($language . '/variableElements.json', $stream); $stream->close(); // render item based on the modified QtiItem $xhtml = $qtiService->renderQTIItem($qtiItem, $language); //note : no need to manually copy qti or other third party lib files, all dependencies are managed by requirejs // write index.html $stream = \GuzzleHttp\Psr7\stream_for($xhtml); $publicDirectory->writePsrStream($language . '/index.html', $stream, 'text/html'); $stream->close(); return new common_report_Report( common_report_Report::TYPE_SUCCESS, __('Successfully compiled "%s"', $language) ); } catch (\tao_models_classes_FileNotFoundException $e) { return new common_report_Report( common_report_Report::TYPE_ERROR, __('Unable to retrieve asset "%s"', $e->getFilePath()) ); } catch (XIncludeException $e) { return new common_report_Report( common_report_Report::TYPE_ERROR, $e->getUserMessage() ); } catch (\Exception $e) { return new common_report_Report( common_report_Report::TYPE_ERROR, $e->getMessage() ); } } /** * @param core_kernel_classes_Resource $item * @param string $lang * @param Directory $publicDirectory * @return qti\Item * @throws taoItems_models_classes_CompilationFailedException */ protected function retrieveAssets(core_kernel_classes_Resource $item, $lang, Directory $publicDirectory) { $qtiItem = Service::singleton()->getDataItemByRdfItem($item, $lang); if (is_null($qtiItem)) { throw new taoItems_models_classes_CompilationFailedException(__('Unable to retrieve item : ' . $item->getLabel())); } $assetParser = new AssetParser($qtiItem, $publicDirectory); $assetParser->setGetSharedLibraries(false); $assetParser->setGetXinclude(false); $resolver = new ItemMediaResolver($item, $lang); $replacementList = []; foreach ($assetParser->extract() as $type => $assets) { foreach ($assets as $assetUrl) { /** @var QtiItemCompilerAssetBlacklist $blacklistService */ $blacklistService = $this->getServiceLocator()->get(QtiItemCompilerAssetBlacklist::SERVICE_ID); if ($blacklistService->isBlacklisted($assetUrl)) { continue; } $mediaAsset = $resolver->resolve($assetUrl); $mediaSource = $mediaAsset->getMediaSource(); $basename = $mediaSource->getBaseName($mediaAsset->getMediaIdentifier()); $replacement = $basename; $count = 0; while (in_array($replacement, $replacementList)) { $dot = strrpos($basename, '.'); $replacement = $dot !== false ? substr($basename, 0, $dot) . '_' . $count . substr($basename, $dot) : $basename . $count; $count++; } $replacementList[$assetUrl] = $replacement; $tmpfile = $mediaSource->download($mediaAsset->getMediaIdentifier()); $fh = fopen($tmpfile, 'r'); $publicDirectory->writeStream($lang . '/' . $replacement, $fh); fclose($fh); unlink($tmpfile); //$fileStream = $mediaSource->getFileStream($mediaAsset->getMediaIdentifier()); //$publicDirectory->writeStream($lang.'/'.$replacement, $fileStream); } } $dom = new \DOMDocument('1.0', 'UTF-8'); if ($dom->loadXML($qtiItem->toXml()) === true) { $xpath = new \DOMXPath($dom); $attributeNodes = $xpath->query('//@*'); foreach ($attributeNodes as $node) { if (isset($replacementList[$node->value])) { $node->value = $replacementList[$node->value]; } } //@TODO : Fix me please $attributeNodes = $xpath->query("//*[local-name()='entry']|//*[local-name()='property']") ?: []; unset($xpath); foreach ($attributeNodes as $node) { if ($node->nodeValue) { $node->nodeValue = strtr(htmlentities($node->nodeValue, ENT_XML1), $replacementList); } } } else { throw new taoItems_models_classes_CompilationFailedException('Unable to load XML'); } $qtiParser = new Parser($dom->saveXML()); $assetRetrievedQtiItem = $qtiParser->load(); //loadxinclude $xincludeLoader = new XIncludeLoader($assetRetrievedQtiItem, $resolver); $xincludeLoader->load(false); return $assetRetrievedQtiItem; } /** * @param string $uri * @param Item $qtiItem * @param $language */ protected function compileItemIndex($uri, Item $qtiItem, $language) { $context = $this->getContext(); if ($context && $context instanceof ItemCompilerIndex) { $context->setItem($uri, $language, $qtiItem->getAttributeValues()); } } }