385 lines
12 KiB
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);
|
||
|
}
|
||
|
}
|