Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
SharedStimulusImporter
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 14
1406
0.00% covered (danger)
0.00%
0 / 1
 setInstanceUri
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getLabel
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getForm
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 import
0.00% covered (danger)
0.00%
0 / 75
0.00% covered (danger)
0.00%
0 / 1
306
 isValidSharedStimulus
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 hasInteraction
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
2
 hasFeedback
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 hasTemplate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasComponents
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setZipImporter
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getZipImporter
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getTaskParameters
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getMediaService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSharedStimulusStoreService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2014-2020 (original work) Open Assessment Technologies SA;
19 */
20
21declare(strict_types=1);
22
23namespace oat\taoMediaManager\model;
24
25use common_exception_Error;
26use common_exception_UserReadableException;
27use core_kernel_classes_Class;
28use core_kernel_classes_Resource;
29use Exception;
30use helpers_File;
31use oat\oatbox\filesystem\File;
32use common_report_Report as Report;
33use oat\oatbox\log\LoggerAwareTrait;
34use oat\oatbox\log\TaoLoggerAwareInterface;
35use oat\oatbox\service\ConfigurableService;
36use oat\oatbox\service\ServiceManager;
37use oat\taoMediaManager\model\sharedStimulus\parser\SharedStimulusMediaExtractor;
38use oat\taoMediaManager\model\sharedStimulus\service\StoreService;
39use tao_helpers_File;
40use tao_helpers_form_Form as Form;
41use oat\tao\model\import\ImportHandlerHelperTrait;
42use oat\tao\model\import\TaskParameterProviderInterface;
43use qtism\data\QtiComponent;
44use qtism\data\storage\xml\XmlDocument;
45use qtism\data\storage\xml\XmlStorageException;
46use tao_helpers_Uri;
47use tao_models_classes_import_ImportHandler;
48
49/**
50 * @access  public
51 * @package taoMediaManager
52 */
53class SharedStimulusImporter extends ConfigurableService implements
54    tao_models_classes_import_ImportHandler,
55    TaskParameterProviderInterface,
56    TaoLoggerAwareInterface
57{
58    use ImportHandlerHelperTrait {
59        getTaskParameters as getDefaultTaskParameters;
60    }
61    use LoggerAwareTrait;
62
63    /** @var SharedStimulusPackageImporter */
64    private $zipImporter = null;
65
66    /** @var string */
67    private $instanceUri;
68
69    public function setInstanceUri(string $instanceUri): self
70    {
71        $this->instanceUri = $instanceUri;
72
73        return $this;
74    }
75
76    /**
77     * Returns a textual description of the import format
78     *
79     * @return string
80     */
81    public function getLabel()
82    {
83        return __('Shared Stimulus');
84    }
85
86    /**
87     * Returns a form in order to prepare the import
88     * if the import is from a file, the form should include the file element
89     *
90     * @return Form
91     */
92    public function getForm()
93    {
94        return (new FileImportForm($this->instanceUri))
95            ->getForm();
96    }
97
98    /**
99     * Starts the import based on the form
100     *
101     * @param core_kernel_classes_Class $class
102     * @param Form|array $form
103     * @param string|null $userId owner of the resource
104     *
105     * @return Report $report
106     *
107     * @throws common_exception_Error
108     */
109    public function import($class, $form, $userId = null)
110    {
111        $uploadedFile = $this->fetchUploadedFile($form);
112
113        try {
114            $service = $this->getMediaService();
115            $classUri = $class->getUri();
116
117            $instanceUri = $form instanceof Form
118                ? $form->getValue('instanceUri')
119                : (isset($form['instanceUri']) ? $form['instanceUri'] : null);
120
121            $fileInfo = $form instanceof Form ? $form->getValue('source') : $form['source'];
122
123            // importing new media
124            if (!$instanceUri || $instanceUri === $classUri) {
125                //if the file is a zip do a zip import
126                if (!helpers_File::isZipMimeType($fileInfo['type'])) {
127                    try {
128                        self::isValidSharedStimulus($uploadedFile);
129
130                        $directory = $this->getSharedStimulusStoreService()->store(
131                            $uploadedFile,
132                            $fileInfo['name']
133                        );
134
135                        $mediaResourceUri = $this->getMediaService()->createSharedStimulusInstance(
136                            $directory . DIRECTORY_SEPARATOR . $fileInfo['name'],
137                            $classUri,
138                            tao_helpers_Uri::decode($form instanceof Form ? $form->getValue('lang') : $form['lang']),
139                            $userId
140                        );
141
142                        if (!$mediaResourceUri) {
143                            $report = Report::createFailure(__('Fail to import Shared Stimulus'));
144                            $report->setData(['uriResource' => '']);
145                        } else {
146                            $report = Report::createSuccess(__('Shared Stimulus imported successfully'));
147                            $report->add(Report::createSuccess(
148                                __('Imported %s', $fileInfo['name']),
149                                // 'uriResource' key is needed by javascript in tao/views/templates/form/import.tpl
150                                ['uriResource' => $mediaResourceUri]
151                            ));
152                        }
153                    } catch (XmlStorageException $e) {
154                        // The shared stimulus is not qti compliant, display error
155                        $report = Report::createFailure($e->getMessage());
156                        $report->setData(['uriResource' => '']);
157                    }
158                } else {
159                    $report = $this->getZipImporter()->import($class, $form, $userId);
160                }
161            } else {
162                if (!helpers_File::isZipMimeType($fileInfo['type'])) {
163                    self::isValidSharedStimulus($uploadedFile);
164
165                    if (in_array($fileInfo['type'], ['application/xml', 'text/xml'])) {
166                        $name = basename($fileInfo['name'], 'xml');
167                        $name .= 'xhtml';
168                        $filepath = tao_helpers_File::concat([dirname($fileInfo['name']), $name]);
169                        $fileResource = fopen($filepath, 'w');
170                        $uploadedFileResource = $uploadedFile->readStream();
171                        stream_copy_to_stream($uploadedFileResource, $fileResource);
172                        fclose($fileResource);
173                        fclose($uploadedFileResource);
174                    }
175
176                    //Todo: fix replace shared stimulus
177                    $instanceEdited = $service->editMediaInstance(
178                        isset($filepath) ? $filepath : $uploadedFile,
179                        $instanceUri,
180                        tao_helpers_Uri::decode($form instanceof Form ? $form->getValue('lang') : $form['lang']),
181                        $userId
182                    );
183
184                    if (!$instanceEdited) {
185                        $report = Report::createFailure(__('Fail to edit shared stimulus'));
186                    } else {
187                        $report = Report::createSuccess(__('Shared Stimulus edited successfully'));
188                        $report->add(
189                            Report::createSuccess(
190                                __('Edited %s', $fileInfo['name']),
191                                [
192                                    'uriResource' => $instanceUri
193                                ]
194                            )
195                        );
196                    }
197
198                    $report->setData(['uriResource' => $instanceUri]);
199                } else {
200                    $report = $this->getZipImporter()->edit(
201                        new core_kernel_classes_Resource($instanceUri),
202                        $form,
203                        $userId
204                    );
205                }
206            }
207        } catch (Exception $e) {
208            $message = $e instanceof common_exception_UserReadableException
209                ? $e->getUserMessage()
210                : __('An error has occurred. Please contact your administrator.');
211            $report = Report::createFailure($message);
212            $report->setData(['uriResource' => '']);
213            $this->logError($e->getMessage());
214        }
215
216        $this->getUploadService()->remove($uploadedFile);
217
218        return $report;
219    }
220
221    /**
222     * @param string|File $file
223     *
224     * @return XmlDocument
225     *
226     * @throws XmlStorageException
227     */
228    public static function isValidSharedStimulus($file)
229    {
230        // No $version given = auto detect.
231        $xmlDocument = new XmlDocument();
232
233        // don't validate because of APIP
234        if ($file instanceof File) {
235            $xml = $file->read();
236            $xmlDocument->loadFromString($xml, false);
237        } elseif (is_file($file) && is_readable($file)) {
238            $xmlDocument->load($file, false);
239            $xml = file_get_contents($file);
240        }
241
242        // The shared stimulus is qti compliant, see if it is not an interaction, feedback or template
243        if (self::hasInteraction($xmlDocument->getDocumentComponent())) {
244            throw new XmlStorageException("The shared stimulus contains interactions QTI components.");
245        }
246        if (self::hasFeedback($xmlDocument->getDocumentComponent())) {
247            throw new XmlStorageException("The shared stimulus contains feedback QTI components.");
248        }
249
250        if (self::hasTemplate($xmlDocument->getDocumentComponent())) {
251            throw new XmlStorageException("The shared stimulus contains template QTI components.");
252        }
253
254        ServiceManager::getServiceManager()->get(SharedStimulusMediaExtractor::class)
255            ->assertMediaFileExists($xml);
256
257        return $xmlDocument;
258    }
259
260    private static function hasInteraction(QtiComponent $domDocument): bool
261    {
262        $interactions = [
263            'endAttemptInteraction',
264            'inlineChoiceInteraction',
265            'textEntryInteraction',
266            'associateInteraction',
267            'choiceInteraction',
268            'drawingInteraction',
269            'extendedTextInteraction',
270            'gapMatchInteraction',
271            'graphicAssociateInteraction',
272            'graphicGapMatchInteraction',
273            'graphicOrderInteraction',
274            'hotspotInteraction',
275            'selectPointInteraction',
276            'hottextInteraction',
277            'matchInteraction',
278            'mediaInteraction',
279            'orderInteraction',
280            'sliderInteraction',
281            'uploadInteraction',
282            'customInteraction',
283            'positionObjectInteraction',
284
285        ];
286
287        return self::hasComponents($domDocument, $interactions);
288    }
289
290    private static function hasFeedback(QtiComponent $domDocument): bool
291    {
292        $feedback = [
293            'feedbackBlock',
294            'feedbackInline'
295        ];
296
297        return self::hasComponents($domDocument, $feedback);
298    }
299
300    private static function hasTemplate(QtiComponent $domDocument): bool
301    {
302        return self::hasComponents($domDocument, 'templateDeclaration');
303    }
304
305    /**
306     * @param string|string[] $className
307     */
308    private static function hasComponents(QtiComponent $domDocument, $className): bool
309    {
310        return $domDocument->getComponentsByClassName($className)->count() > 0;
311    }
312
313    /**
314     * @param SharedStimulusPackageImporter $zipImporter
315     * @return $this
316     */
317    public function setZipImporter($zipImporter)
318    {
319        $this->zipImporter = $zipImporter;
320
321        return $this;
322    }
323
324    /**
325     * Get the zip importer for shared stimulus
326     *
327     * @return SharedStimulusPackageImporter
328     */
329    protected function getZipImporter()
330    {
331        if (!$this->zipImporter) {
332            $this->zipImporter = new SharedStimulusPackageImporter();
333            $this->zipImporter->setServiceLocator($this->getServiceLocator());
334        }
335
336        return $this->zipImporter;
337    }
338
339    /**
340     * Defines the task parameters to be stored for later use.
341     *
342     * @param Form $form
343     * @return array
344     */
345    public function getTaskParameters(Form $form)
346    {
347        return array_merge(
348            $form->getValues(),
349            $this->getDefaultTaskParameters($form)
350        );
351    }
352
353    private function getMediaService(): MediaService
354    {
355        return $this->getServiceLocator()->get(MediaService::class);
356    }
357
358    private function getSharedStimulusStoreService(): StoreService
359    {
360        return $this->getServiceLocator()->get(StoreService::class);
361    }
362}