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); } }