tao-test/app/taoQtiItem/model/Export/AbstractQTIItemExporter.php

320 lines
12 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2008-2010 (original work) Deutsche Institut für Internationale Pädagogische Forschung (under the project TAO-TRANSFER);
* 2009-2012 (update and modification) Public Research Centre Henri Tudor (under the project TAO-SUSTAIN & TAO-DEV);
* 2013-2020 (update and modification) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*/
namespace oat\taoQtiItem\model\Export;
use oat\tao\helpers\Base64;
use core_kernel_classes_Property;
use DOMDocument;
use League\Flysystem\FileNotFoundException;
use oat\oatbox\filesystem\Directory;
use oat\oatbox\service\ServiceManager;
use oat\tao\model\media\ProcessedFileStreamAware;
use oat\tao\model\media\sourceStrategy\HttpSource;
use oat\taoItems\model\media\ItemMediaResolver;
use oat\taoItems\model\media\LocalItemSource;
use oat\taoQtiItem\model\portableElement\exception\PortableElementException;
use oat\taoQtiItem\model\portableElement\exception\PortableElementInvalidAssetException;
use oat\taoQtiItem\model\portableElement\PortableElementService;
use oat\taoQtiItem\model\qti\AssetParser;
use oat\taoQtiItem\model\qti\Element;
use oat\taoQtiItem\model\qti\exception\ExportException;
use oat\taoQtiItem\model\qti\metadata\exporter\MetadataExporter;
use oat\taoQtiItem\model\qti\metadata\MetadataService;
use oat\taoQtiItem\model\qti\Service;
use Psr\Http\Message\StreamInterface;
use taoItems_models_classes_ItemExporter;
abstract class AbstractQTIItemExporter extends taoItems_models_classes_ItemExporter
{
/**
* List of regexp of media that should be excluded from retrieval
*/
private static $BLACKLIST = [
'/^data:[^\/]+\/[^;]+(;charset=[\w]+)?;base64,/'
];
/**
* @var MetadataExporter Service to export metadata to IMSManifest
*/
protected $metadataExporter;
abstract public function buildBasePath();
abstract protected function renderManifest(array $options, array $qtiItemData);
abstract protected function itemContentPostProcessing($content);
/**
* Overriden export from QTI items.
*
* @see taoItems_models_classes_ItemExporter::export()
* @param array $options An array of options.
* @return \common_report_Report
* @throws ExportException
* @throws \common_Exception
* @throws \common_exception_Error
* @throws \core_kernel_persistence_Exception
* @throws PortableElementInvalidAssetException
*/
public function export($options = [])
{
$report = \common_report_Report::createSuccess();
$asApip = isset($options['apip']) && $options['apip'] === true;
$lang = \common_session_SessionManager::getSession()->getDataLanguage();
$basePath = $this->buildBasePath();
if (is_null($this->getItemModel())) {
$report->setMessage($this->getExportErrorMessage(__('not a QTI item')));
$report->setType(\common_report_Report::TYPE_ERROR);
return $report;
}
$dataFile = (string) $this->getItemModel()->getOnePropertyValue(new core_kernel_classes_Property(\taoItems_models_classes_ItemsService::TAO_ITEM_MODEL_DATAFILE_PROPERTY));
$resolver = new ItemMediaResolver($this->getItem(), $lang);
$replacementList = [];
$portableElements = $this->getPortableElementAssets($this->getItem(), $lang);
$service = new PortableElementService();
$service->setServiceLocator(ServiceManager::getServiceManager());
$portableElementsToExport = [];
$portableAssets = [];
foreach ($portableElements as $element) {
if (! $element instanceof Element) {
continue;
}
try {
$object = $service->getPortableObjectFromInstance($element);
} catch (PortableElementException $e) {
$message = __('Fail to export item') . ' (' . $this->getItem()->getLabel() . '): ' . $e->getMessage();
return \common_report_Report::createFailure($message);
}
$portableElementExporter = $object->getModel()->getExporter($object, $this);
$portableElementsToExport[$element->getTypeIdentifier()] = $portableElementExporter;
try {
$portableAssets = array_merge($portableAssets, $portableElementExporter->copyAssetFiles($replacementList));
} catch (\tao_models_classes_FileNotFoundException $e) {
\common_Logger::i($e->getMessage());
$report->setMessage('Missing portable element asset for "' . $object->getTypeIdentifier() . '"": ' . $e->getMessage());
$report->setType(\common_report_Report::TYPE_ERROR);
}
}
$assets = $this->getAssets($this->getItem(), $lang);
foreach ($assets as $assetUrl) {
try {
$mediaAsset = $resolver->resolve($assetUrl);
$mediaSource = $mediaAsset->getMediaSource();
$mediaIdentifier = $mediaAsset->getMediaIdentifier();
if (!$mediaSource instanceof HttpSource && !Base64::isEncodedImage($mediaIdentifier)) {
$link = $mediaIdentifier;
if ($mediaSource instanceof ProcessedFileStreamAware) {
$stream = $mediaSource->getProcessedFileStream($link);
} else {
$stream = $mediaSource->getFileStream($link);
}
$baseName = ($mediaSource instanceof LocalItemSource) ? $link : 'assets/' . $mediaSource->getBaseName($link);
$replacement = $this->copyAssetFile($stream, $basePath, $baseName, $replacementList);
$replacementList[$assetUrl] = $replacement;
}
} catch (\tao_models_classes_FileNotFoundException $e) {
$replacementList[$assetUrl] = '';
$report->setMessage('Missing resource for ' . $assetUrl);
$report->setType(\common_report_Report::TYPE_ERROR);
}
}
try {
$xml = Service::singleton()->getXmlByRdfItem($this->getItem());
} catch (FileNotFoundException $e) {
$report->setMessage($this->getExportErrorMessage(__('cannot find QTI XML')));
$report->setType(\common_report_Report::TYPE_ERROR);
return $report;
}
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->preserveWhiteSpace = false;
$dom->formatOutput = true;
if ($dom->loadXML($xml) === true) {
$xpath = new \DOMXPath($dom);
$attributeNodes = $xpath->query('//@*');
$portableEntryNodes = $xpath->query("//*[local-name()='entry']|//*[local-name()='property']") ?: [];
unset($xpath);
foreach ($attributeNodes as $node) {
if (isset($replacementList[$node->value])) {
$node->value = $replacementList[$node->value];
}
}
foreach ($portableEntryNodes as $node) {
$node->nodeValue = strtr(htmlentities($node->nodeValue, ENT_XML1), $replacementList);
}
foreach ($portableElementsToExport as $portableElementExporter) {
$portableElementExporter->exportDom($dom);
}
} else {
$report->setMessage($this->getExportErrorMessage(__('cannot load QTI XML')));
$report->setType(\common_report_Report::TYPE_ERROR);
return $report;
}
if (($content = $dom->saveXML()) === false) {
$report->setMessage($this->getExportErrorMessage(__('invalid QTI XML')));
$report->setType(\common_report_Report::TYPE_ERROR);
}
// Possibility to delegate (if necessary) some item content post-processing to sub-classes.
$content = $this->itemContentPostProcessing($content);
// add xml file
$this->getZip()->addFromString($basePath . '/' . $dataFile, $content);
if (! $report->getMessage()) {
$report->setMessage(__('Item "%s" is ready to be exported', $this->getItem()->getLabel()));
}
///return some useful data to the export report
$report->setData(['portableAssets' => $portableAssets]);
return $report;
}
/**
* Format a consistent error reporting message
*
* @param string $errorMessage
* @return string
*/
private function getExportErrorMessage($errorMessage)
{
return __('Item "%s" cannot be exported: %s', $this->getItem()->getLabel(), $errorMessage);
}
public function copyAssetFile(StreamInterface $stream, $basePath, $baseName, &$replacementList)
{
$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++;
}
// To check if replacement is to replace basename ???
// Please check it seriously next time!
$newRelPath = (empty($basePath) ? '' : $basePath . '/' ) . preg_replace('/^(.\/)/', '', $replacement);
$this->addFile($stream, $newRelPath);
$stream->close();
return $replacement;
}
/**
* Get the item's assets
*
* @param \core_kernel_classes_Resource $item The item
* @param string $lang The item lang
*
* @return string[] The assets URLs
*/
protected function getAssets(\core_kernel_classes_Resource $item, $lang)
{
$qtiItem = Service::singleton()->getDataItemByRdfItem($item, $lang);
if (is_null($qtiItem)) {
return [];
}
$assetParser = new AssetParser($qtiItem, $this->getStorageDirectory($item, $lang));
$assetParser->setGetSharedLibraries(false);
$returnValue = [];
foreach ($assetParser->extract() as $type => $assets) {
foreach ($assets as $assetUrl) {
foreach (self::$BLACKLIST as $blacklist) {
if (preg_match($blacklist, $assetUrl) === 1) {
continue(2);
}
}
$returnValue[] = $assetUrl;
}
}
return $returnValue;
}
protected function getPortableElementAssets(\core_kernel_classes_Resource $item, $lang)
{
$qtiItem = Service::singleton()->getDataItemByRdfItem($item, $lang);
if (is_null($qtiItem)) {
return [];
}
$directory = $this->getStorageDirectory($item, $lang);
$assetParser = new AssetParser($qtiItem, $directory);
$assetParser->setGetCustomElementDefinition(true);
return $assetParser->extractPortableAssetElements();
}
/**
* Get the item's directory
*
* @param \core_kernel_classes_Resource $item The item
* @param string $lang The item lang
*
* @return Directory The directory
*/
protected function getStorageDirectory(\core_kernel_classes_Resource $item, $lang)
{
$itemService = \taoItems_models_classes_ItemsService::singleton();
$directory = $itemService->getItemDirectory($item, $lang);
//we should use be language unaware for storage manipulation
$path = str_replace($lang, '', $directory->getPrefix());
return $itemService->getDefaultItemDirectory()->getDirectory($path);
}
protected function getServiceManager()
{
return ServiceManager::getServiceManager();
}
/**
* Get the service to export Metadata
*
* @return MetadataExporter
*/
protected function getMetadataExporter()
{
if (! $this->metadataExporter) {
$this->metadataExporter = $this->getServiceManager()->get(MetadataService::SERVICE_ID)->getExporter();
}
return $this->metadataExporter;
}
}