tao-test/app/taoQtiItem/model/portableElement/parser/itemParser/PortableElementItemParser.php

432 lines
14 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) 2016 (original work) Open Assessment Technologies SA;
*
*/
namespace oat\taoQtiItem\model\portableElement\parser\itemParser;
use League\Flysystem\FileNotFoundException;
use oat\taoQtiItem\model\portableElement\exception\PortableElementInconsistencyModelException;
use oat\taoQtiItem\model\portableElement\element\PortableElementObject;
use oat\taoQtiItem\model\portableElement\model\PortableModelRegistry;
use oat\taoQtiItem\model\portableElement\model\PortableElementModel;
use oat\taoQtiItem\model\portableElement\PortableElementService;
use oat\taoQtiItem\model\qti\Item;
use oat\taoQtiItem\model\qti\Element;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorAwareTrait;
class PortableElementItemParser implements ServiceLocatorAwareInterface
{
use ServiceLocatorAwareTrait;
/**
* @var Item
*/
protected $qtiModel;
protected $importingFiles = [];
protected $requiredFiles = [];
protected $portableObjects = [];
protected $picModels = [];
protected $source;
protected $itemDir;
/**
* @var PortableElementService
*/
protected $service;
/**
* @return PortableElementService
*/
public function getService()
{
if (!$this->service) {
$this->service = new PortableElementService();
$this->service->setServiceLocator($this->getServiceLocator());
}
return $this->service;
}
/**
* @return PortableModelRegistry
*/
protected function getPortableFactory()
{
return PortableModelRegistry::getRegistry();
}
/**
* Handle pci import process for a file
*
* @param $absolutePath
* @param $relativePath
* @return array
* @throws \common_Exception
* @throws \tao_models_classes_FileNotFoundException
*/
public function importPortableElementFile($absolutePath, $relativePath)
{
if ($this->isPortableElementAsset($relativePath)) {
//marked the file as being ok to be imported in the end
$this->importingFiles[] = $relativePath;
//@todo remove qti file used by PCI
return $this->getFileInfo($absolutePath, $relativePath);
} else {
throw new \common_Exception('trying to import an asset that is not part of the portable element asset list');
}
}
/**
* Check if Item contains portable element
*
* @return bool
*/
public function hasPortableElement()
{
return (count($this->requiredFiles) > 0);
}
/**
* Check if file is required by a portable element
*
* @param $fileRelativePath
* @return bool
*/
public function isPortableElementAsset($fileRelativePath)
{
return isset($this->requiredFiles[$fileRelativePath]);
}
/**
* Get details about file
*
* @param $path
* @param $relPath
* @return array
* @throws \tao_models_classes_FileNotFoundException
*/
public function getFileInfo($path, $relPath)
{
if (file_exists($path)) {
return [
'name' => basename($path),
'uri' => $relPath,
'mime' => \tao_helpers_File::getMimeType($path),
'filePath' => $path,
'size' => filesize($path),
];
}
throw new \tao_models_classes_FileNotFoundException($path);
}
/**
* @return Item
*/
public function getQtiModel()
{
return $this->qtiModel;
}
/**
*
* @param Item $item
* @return $this
*/
public function setQtiModel(Item $item)
{
$this->qtiModel = $item;
$this->feedRequiredFiles($item);
return $this;
}
/**
* Feed the instance with portable related data extracted from the item
*
* @param Item $item
* @throws \common_Exception
*/
protected function feedRequiredFiles(Item $item)
{
$this->requiredFiles = [];
$this->portableObjects = [];
$this->picModels = [];
$models = $this->getPortableFactory()->getModels();
foreach ($models as $model) {
$className = $model->getQtiElementClassName();
$portableElementsXml = $item->getComposingElements($className);
foreach ($portableElementsXml as $portableElementXml) {
$this->parsePortableElement($model, $portableElementXml);
}
}
}
protected function getSourceAdjustedNodulePath($path)
{
$realpath = realpath($this->itemDir . DIRECTORY_SEPARATOR . $path);
$sourcePath = realpath($this->source);
return str_replace($sourcePath . DIRECTORY_SEPARATOR, '', $realpath);
}
/**
* Parse individual portable element into the given portable model
* @param PortableElementModel $model
* @param Element $portableElement
* @throws \common_Exception
* @throws PortableElementInconsistencyModelException
*/
protected function parsePortableElement(PortableElementModel $model, Element $portableElement)
{
$typeId = $portableElement->getTypeIdentifier();
$libs = [];
$librariesFiles = [];
$entryPoint = [];
//Adjust file resource entries where {QTI_NS}/xxx/yyy is equivalent to {QTI_NS}/xxx/yyy.js
foreach ($portableElement->getLibraries() as $lib) {
if (preg_match('/^' . $typeId . '/', $lib) && substr($lib, -3) != '.js') {//filter shared stimulus
$librariesFiles[] = $lib . '.js';//amd modules
$libs[] = $lib . '.js';
} else {
$libs[] = $lib;//shared libs
}
}
$moduleFiles = [];
$emptyModules = [];//list of modules that are referenced directly in the module node
$adjustedModules = [];
foreach ($portableElement->getModules() as $id => $paths) {
$adjustedPaths = [];
if (empty($paths)) {
$emptyModules[] = $id;
continue;
}
foreach ($paths as $path) {
if ($this->isRelativePath($path)) {
//only copy into data the relative files
$moduleFiles[] = $path;
$adjustedPaths[] = $this->getSourceAdjustedNodulePath($path);
} else {
$adjustedPaths[] = $path;
}
}
$adjustedModules[$id] = $adjustedPaths;
}
/**
* Parse the standard portable configuration if applicable.
* Local config files will be preloaded into the registry itself and the registered modules will be included as required dependency files.
* Per standard, every config file have the following structure:
* {
* "waitSeconds": 15,
* "paths": {
* "graph": "https://example.com/js/modules/graph1.01/graph.js",
* "foo": "foo/bar1.2/foo.js"
* }
* }
*/
$configDataArray = [];
$configFiles = [];
foreach ($portableElement->getConfig() as $configFile) {
//only read local config file
if ($this->isRelativePath($configFile)) {
//save the content and file config data in registry, to allow later retrieval
$configFiles[] = $configFile;
//read the config file content
$configData = json_decode(file_get_contents($this->itemDir . DIRECTORY_SEPARATOR . $configFile), true);
if (!empty($configData)) {
if (isset($configData['paths'])) {
foreach ($configData['paths'] as $id => $path) {
//only copy the relative files to local portable element filesystem, absolute ones are loaded dynamically
if ($this->isRelativePath($path)) {
//resolution of path, relative to the current config file it has been defined in
$path = dirname($configFile) . DIRECTORY_SEPARATOR . $path;
if (file_exists($this->itemDir . DIRECTORY_SEPARATOR . $path)) {
$moduleFiles[] = $path;
$configData['paths'][$id] = $this->getSourceAdjustedNodulePath($path);
;
} else {
throw new FileNotFoundException("The portable config {$configFile} references a missing module file {$id} => {$path}");
}
}
}
}
$configDataArray[] = [
'file' => $this->getSourceAdjustedNodulePath($configFile),
'data' => $configData
];
}
} else {
$configDataArray[] = ['file' => $configFile];
}
}
/**
* In the standard IMS PCI, entry points become optionnal
*/
if (!empty($portableElement->getEntryPoint())) {
$entryPoint[] = $portableElement->getEntryPoint();
}
//register the files here
$data = [
'typeIdentifier' => $typeId,
'version' => $portableElement->getVersion(),
'label' => $typeId,
'short' => $typeId,
'runtime' => [
'hook' => $portableElement->getEntryPoint(),
'libraries' => $libs,
'stylesheets' => $portableElement->getStylesheets(),
'mediaFiles' => $portableElement->getMediaFiles(),
'config' => $configDataArray,
'modules' => $adjustedModules
]
];
/** @var PortableElementObject $portableObject */
$portableObject = $model->createDataObject($data);
$lastVersionModel = $this->getService()->getPortableElementByIdentifier(
$portableObject->getModel()->getId(),
$portableObject->getTypeIdentifier()
);
if (
!is_null($lastVersionModel)
&& (intval($lastVersionModel->getVersion()) != intVal($portableObject->getVersion()))
) {
//@todo return a user exception to inform user of incompatible pci version found and that an item update is required
throw new \common_Exception('Unable to import pci asset because pci is not compatible. '
. 'Current version is ' . $lastVersionModel->getVersion() . ' and imported is ' . $portableObject->getVersion());
}
$this->portableObjects[$typeId] = $portableObject;
$files = array_merge(
$entryPoint,
$librariesFiles,
$configFiles,
$moduleFiles,
$portableObject->getRuntimeKey('stylesheets'),
$portableObject->getRuntimeKey('mediaFiles')
);
$this->requiredFiles = array_merge($this->requiredFiles, array_fill_keys($files, $typeId));
}
/**
* Set the root directory of the QTI package, where the qti manifest.xml is located
*
* @param $source
* @return $this
*/
public function setSource($source)
{
$this->source = $source;
return $this;
}
/**
* Set the directory where the qti item qti.xml file is locate
*
* @param $itemDir
* @return $this
*/
public function setItemDir($itemDir)
{
$this->itemDir = $itemDir;
return $this;
}
/**
* Get the parsed portable objects
*
* @return array
*/
public function getPortableObjects()
{
return $this->portableObjects;
}
/**
* Do the import of portable elements
*/
public function importPortableElements()
{
if (count($this->importingFiles) != count($this->requiredFiles)) {
throw new \common_Exception('Needed files are missing during Portable Element asset files ' . print_r($this->requiredFiles, true) . ' ' . print_r($this->importingFiles, true));
}
/** @var PortableElementObject $object */
foreach ($this->portableObjects as $object) {
$lastVersionModel = $this->getService()->getPortableElementByIdentifier(
$object->getModel()->getId(),
$object->getTypeIdentifier()
);
//only register a pci that has not been register yet, subsequent update must be done through pci package import
if (is_null($lastVersionModel)) {
$this->getService()->registerModel(
$object,
$object->getRegistrationSourcePath($this->source, $this->itemDir)
);
} else {
\common_Logger::i('The imported item contains the portable element ' . $object->getTypeIdentifier()
. ' in a version ' . $object->getVersion() . ' compatible with the current ' . $lastVersionModel->getVersion());
}
}
return true;
}
/**
* Replace the libs aliases with their relative url before saving into the registry
* This format is consistent with the format of TAO portable package manifest
*
* @param PortableElementObject $object
* @return PortableElementObject
*/
private function replaceLibAliases(PortableElementObject $object)
{
$id = $object->getTypeIdentifier();
$object->setRuntimeKey('libraries', array_map(function ($lib) use ($id) {
if (preg_match('/^' . $id . '/', $lib)) {
return $lib . '.js';
}
return $lib;
}, $object->getRuntimeKey('libraries')));
return $object;
}
private function isRelativePath($path)
{
return (strpos($path, 'http') !== 0);
}
}