tao-test/app/taoMediaManager/model/SharedStimulusPackageImporter.php

385 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) 2014-2021 (original work) Open Assessment Technologies SA;
*/
declare(strict_types=1);
namespace oat\taoMediaManager\model;
use common_Exception;
use common_exception_Error;
use common_exception_UserReadableException;
use common_report_Report as Report;
use core_kernel_classes_Class;
use core_kernel_classes_Resource as Resource;
use Exception;
use helpers_File;
use oat\tao\model\import\InvalidSourcePathException;
use oat\taoMediaManager\model\sharedStimulus\service\StoreService;
use qtism\data\content\xhtml\Img;
use qtism\data\content\xhtml\QtiObject;
use qtism\data\storage\xml\XmlDocument;
use qtism\data\storage\xml\XmlStorageException;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use SplFileInfo;
use tao_helpers_File;
use tao_helpers_form_Form as Form;
use tao_helpers_Uri;
use tao_models_classes_FileNotFoundException;
/**
* Service methods to manage the Media
*
* @access public
* @package taoMediaManager
*/
class SharedStimulusPackageImporter extends ZipImporter
{
/**
* Starts the import based on the form
*
* @param core_kernel_classes_Class $class
* @param Form|array $form
* @param string|null $userId owner of the resource
* @return Report
*/
public function import($class, $form, $userId = null)
{
try {
$uploadedFile = $this->fetchUploadedFile($form);
$extractPath = $this->extractArchive($uploadedFile);
$xmlFile = $this->getSharedStimulusFile($extractPath);
$cssFiles = $this->getSharedStimulusStylesheets($extractPath);
$this->getUploadService()->remove($uploadedFile);
// throws an exception of invalid
SharedStimulusImporter::isValidSharedStimulus($xmlFile);
$embeddedFile = static::embedAssets($xmlFile);
$report = Report::createSuccess(__('Shared Stimulus imported successfully'));
$subReport = $this->storeSharedStimulus(
$class,
$this->getDecodedUri($form),
$embeddedFile,
$cssFiles,
$userId
);
$report->add($subReport);
} catch (Exception $e) {
$message = $e instanceof common_exception_UserReadableException
? $e->getUserMessage()
: __('An error has occurred. Please contact your administrator.');
$report = Report::createFailure($message);
$this->logError($e->getMessage());
}
return $report;
}
/**
* Edit a shared stimulus package
*
* @param Resource $instance
* @param Form|array $form
* @param null|string $userId
* @return Report
*/
public function edit(Resource $instance, $form, $userId = null)
{
try {
$uploadedFile = $this->fetchUploadedFile($form);
$extractPath = $this->extractArchive($uploadedFile);
$xmlFile = $this->getSharedStimulusFile($extractPath);
$this->getUploadService()->remove($uploadedFile);
// throws an exception of invalid
SharedStimulusImporter::isValidSharedStimulus($xmlFile);
$embeddedFile = static::embedAssets($xmlFile);
$report = $this->replaceSharedStimulus($instance, $this->getDecodedUri($form), $embeddedFile, $userId);
} catch (Exception $e) {
$message = $e instanceof common_exception_UserReadableException
? $e->getUserMessage()
: __('An error has occurred. Please contact your administrator.');
$report = Report::createFailure($message);
$this->logError($e->getMessage());
$report->setData(['uriResource' => '']);
}
return $report;
}
/**
* Embed external resources into the XML
*
* @param $originalXml
*
* @return string
* @throws InvalidSourcePathException
* @throws common_exception_Error
* @throws XmlStorageException
* @throws tao_models_classes_FileNotFoundException
*/
public static function embedAssets($originalXml)
{
$basedir = dirname($originalXml) . DIRECTORY_SEPARATOR;
$xmlDocument = new XmlDocument();
$xmlDocument->load($originalXml, true);
//get images and object to base64 their src/data
$images = $xmlDocument->getDocumentComponent()->getComponentsByClassName('img');
$objects = $xmlDocument->getDocumentComponent()->getComponentsByClassName('object');
/** @var $image Img */
foreach ($images as $image) {
$source = $image->getSrc();
static::validateSource($basedir, $source);
$image->setSrc(self::secureEncode($basedir, $source));
}
/** @var $object QtiObject */
foreach ($objects as $object) {
$data = $object->getData();
static::validateSource($basedir, $data);
$object->setData(self::secureEncode($basedir, $data));
}
// save the document to a tempfile
$newXml = tempnam(sys_get_temp_dir(), 'sharedStimulus_') . '.xml';
$xmlDocument->save($newXml);
return $newXml;
}
/**
* @param string $basePath
* @param string $sourcePath
*
* @throws InvalidSourcePathException
*/
private static function validateSource(string $basePath, string $sourcePath): void
{
$urlData = parse_url($sourcePath);
if (!empty($urlData['scheme'])) {
return;
}
if (!helpers_File::isFileInsideDirectory($sourcePath, $basePath)) {
throw new InvalidSourcePathException($basePath, $sourcePath);
}
}
/**
* Get the shared stimulus file with assets from the zip
*
* @return string path to the xml
*
* @throws common_Exception
*/
private function getSharedStimulusFile(string $extractPath): string
{
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($extractPath),
RecursiveIteratorIterator::LEAVES_ONLY
);
/** @var $file SplFileInfo */
foreach ($iterator as $file) {
//check each file to see if it can be the shared stimulus file
if ($this->isFileExtension($file, 'xml')) {
return $file->getRealPath();
}
}
throw new common_Exception('XML not found in the package');
}
/**
* Get an additional CSS stylesheet for the shared stimulus (If exists)
*
* @return array path to the CSS or false if not found
*/
private function getSharedStimulusStylesheets(string $extractPath): array
{
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($extractPath),
RecursiveIteratorIterator::LEAVES_ONLY
);
$cssFileInfoArray = [];
/** @var $file SplFileInfo */
foreach ($iterator as $file) {
if ($this->isFileExtension($file, 'css')) {
$cssFileInfoArray[] = $file->getRealPath();
}
}
return $cssFileInfoArray;
}
public function isFileExtension(SplFileInfo $file, string $extension): bool
{
if ($file->isFile()) {
return preg_match('/^[\w]/', $file->getFilename()) === 1 && $file->getExtension() === $extension;
}
return false;
}
/**
* Convert file linked inside and store it into media manager
*
* @throws common_exception_Error
*/
private function storeSharedStimulus(
Resource $class,
string $lang,
string $xmlFile,
array $cssFiles,
string $userId = null
): Report {
$stimulusFilename = basename($xmlFile);
$directory = $this->getSharedStimulusStoreService()->store(
$xmlFile,
$stimulusFilename,
$cssFiles
);
$mediaResourceUri = $this->getMediaService()->createSharedStimulusInstance(
$directory . DIRECTORY_SEPARATOR . $stimulusFilename,
$class->getUri(),
$lang,
$userId
);
if ($mediaResourceUri !== false) {
$report = Report::createSuccess(__('Imported %s', basename($xmlFile)));
$report->setData(['uriResource' => $mediaResourceUri]);
} else {
$report = Report::createFailure(__('Fail to import Shared Stimulus'));
$report->setData(['uriResource' => '']);
}
return $report;
}
/**
* Validate an xml file, convert file linked inside and store it into media manager
*
* @throws common_exception_Error
* @throws XmlStorageException
*/
protected function replaceSharedStimulus(
Resource $instance,
string $lang,
string $xmlFile,
string $userId = null
): Report {
//if the class does not belong to media classes create a new one with its name (for items)
$mediaClass = new core_kernel_classes_Class(MediaService::ROOT_CLASS_URI);
if (!$instance->isInstanceOf($mediaClass)) {
$report = Report::createFailure(
'The instance ' . $instance->getUri() . ' is not a Media instance'
);
$report->setData(['uriResource' => '']);
return $report;
}
SharedStimulusImporter::isValidSharedStimulus($xmlFile);
$name = basename($xmlFile, '.xml');
$name .= '.xhtml';
$filepath = dirname($xmlFile) . '/' . $name;
tao_helpers_File::copy($xmlFile, $filepath);
if (!$this->getMediaService()->editMediaInstance($filepath, $instance->getUri(), $lang, $userId)) {
$report = Report::createFailure(__('Fail to edit Shared Stimulus'));
} else {
$report = Report::createSuccess(__('Shared Stimulus edited successfully'));
$report->add(
Report::createSuccess(
__('Edited %s', $instance->getLabel()),
[
'uriResource' => $instance->getUri()
]
)
);
}
$report->setData(['uriResource' => $instance->getUri()]);
return $report;
}
/**
* Verify paths and encode the file
*
* @throws tao_models_classes_FileNotFoundException
* @throws common_exception_Error
*/
protected static function secureEncode(string $basedir, string $source): string
{
$components = parse_url($source);
if (!isset($components['scheme'])) {
if (tao_helpers_File::securityCheck($source, false)) {
if (file_exists($basedir . $source)) {
return 'data:' . tao_helpers_File::getMimeType($basedir . $source) . ';'
. 'base64,' . base64_encode(file_get_contents($basedir . $source));
}
throw new tao_models_classes_FileNotFoundException($source);
}
throw new common_exception_Error('Invalid source path "' . $source . '"');
}
return $source;
}
/**
* @param array|Form $form
*/
private function getDecodedUri($form): string
{
return tao_helpers_Uri::decode($form instanceof Form ? $form->getValue('lang') : $form['lang']);
}
private function getMediaService(): MediaService
{
return $this->getServiceLocator()->get(MediaService::class);
}
private function getSharedStimulusStoreService(): StoreService
{
return $this->getServiceLocator()->get(StoreService::class);
}
}