Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
QtiCreator
0.00% covered (danger)
0.00%
0 / 156
0.00% covered (danger)
0.00%
0 / 17
2162
0.00% covered (danger)
0.00%
0 / 1
 getEventManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createItem
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 index
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getMediaSources
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 getItemData
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
 saveItem
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
30
 getFile
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 renderFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getCreatorConfig
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
56
 getUpdatedItemEventDispatcher
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getXmlToItemParser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemIdentifierValidator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFeatureFlagConfigSwitcher
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 validateXmlInput
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getItemIdentifier
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getIdentifierGenerator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFeatureFlagChecker
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) 2013-2019 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 *
21 */
22
23namespace oat\taoQtiItem\controller;
24
25use common_exception_BadRequest;
26use common_exception_Error;
27use core_kernel_classes_Resource;
28use oat\generis\model\data\event\ResourceUpdated;
29use oat\generis\model\OntologyAwareTrait;
30use oat\oatbox\event\EventManager;
31use oat\tao\model\featureFlag\FeatureFlagChecker;
32use oat\tao\model\featureFlag\FeatureFlagCheckerInterface;
33use oat\tao\model\featureFlag\FeatureFlagConfigSwitcher;
34use oat\tao\model\http\HttpJsonResponseTrait;
35use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorInterface;
36use oat\tao\model\IdentifierGenerator\Generator\IdentifierGeneratorProxy;
37use oat\tao\model\media\MediaService;
38use oat\tao\model\TaoOntology;
39use oat\taoItems\model\event\ItemCreatedEvent;
40use oat\taoItems\model\media\ItemMediaResolver;
41use oat\taoQtiItem\helpers\Authoring;
42use oat\taoQtiItem\model\CreatorConfig;
43use oat\taoQtiItem\model\event\ItemCreatorLoad;
44use oat\taoQtiItem\model\HookRegistry;
45use oat\taoQtiItem\model\ItemModel;
46use oat\taoQtiItem\model\qti\event\UpdatedItemEventDispatcher;
47use oat\taoQtiItem\model\qti\exception\QtiModelException;
48use oat\taoQtiItem\model\qti\parser\XmlToItemParser;
49use oat\taoQtiItem\model\qti\Service;
50use oat\taoQtiItem\model\qti\validator\ItemIdentifierValidator;
51use tao_actions_CommonModule;
52use tao_helpers_File;
53use tao_helpers_Http;
54use tao_helpers_Uri;
55use taoItems_models_classes_ItemsService;
56
57/**
58 * QtiCreator Controller provide actions to edit a QTI item
59 *
60 * @author CRP Henri Tudor - TAO Team - {@link http://www.tao.lu}
61 * @package taoQTI
62 * @license GPLv2  http://www.opensource.org/licenses/gpl-2.0.php
63 */
64class QtiCreator extends tao_actions_CommonModule
65{
66    use OntologyAwareTrait;
67    use HttpJsonResponseTrait;
68
69    /**
70     * @return EventManager
71     */
72    protected function getEventManager()
73    {
74        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
75    }
76
77    /**
78     * create a new QTI item
79     *
80     * @throws common_exception_BadRequest
81     * @throws common_exception_Error
82     *
83     * @requiresRight id WRITE
84     * @requiresRight classUri WRITE
85     */
86    public function createItem()
87    {
88        if (!\tao_helpers_Request::isAjax()) {
89            throw new common_exception_BadRequest('wrong request mode');
90        }
91        try {
92            $this->validateCsrf();
93        } catch (\common_exception_Unauthorized $e) {
94            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
95            return;
96        }
97
98        $clazz = new \core_kernel_classes_Resource($this->getRequestParameter('id'));
99        if ($clazz->isClass()) {
100            $clazz = new \core_kernel_classes_Class($clazz);
101        } else {
102            foreach ($clazz->getTypes() as $type) {
103                // determine class from selected instance
104                $clazz = $type;
105                break;
106            }
107        }
108        $service = \taoItems_models_classes_ItemsService::singleton();
109
110        $label = $service->createUniqueLabel($clazz);
111        $item = $service->createInstance($clazz, $label);
112
113        if ($item !== null) {
114            $service->setItemModel($item, new \core_kernel_classes_Resource(ItemModel::MODEL_URI));
115            $this->getEventManager()->trigger(new ItemCreatedEvent($item->getUri()));
116            $response = [
117                'success' => true,
118                'message' => __('Successfully created item "%s"', $item->getLabel()),
119                'label'   => $item->getLabel(),
120                'uri'     => $item->getUri()
121            ];
122        } else {
123            $response = false;
124        }
125        $this->returnJson($response);
126    }
127
128    public function index()
129    {
130
131        if (!$this->hasRequestParameter('instance')) {
132            throw new common_exception_Error('The item creator needs to be opened with an item');
133        }
134        $item = new core_kernel_classes_Resource(tao_helpers_Uri::decode($this->getRequestParameter('instance')));
135
136        $config = $this->getCreatorConfig($item);
137
138        $this->setData('config', $config->toArray());
139        $this->setView('QtiCreator/index.tpl');
140
141        $this->getEventManager()->trigger(new ItemCreatorLoad());
142    }
143
144    public function getMediaSources()
145    {
146        $exclude = '';
147        if ($this->hasRequestParameter('exclude')) {
148            $exclude = $this->getRequestParameter('exclude');
149        }
150        // get the config media Sources
151        $sources = array_keys(MediaService::singleton()->getBrowsableSources());
152        $mediaSources = [];
153        if ($exclude !== 'local') {
154            $mediaSources[] = ['root' => 'local', 'path' => '/'];
155        }
156        foreach ($sources as $source) {
157            if ($source !== $exclude) {
158                $mediaSources[] = ['root' => $source, 'path' => 'taomedia://' . $source . '/'];
159            }
160        }
161
162        $this->returnJson($mediaSources);
163    }
164
165    public function getItemData()
166    {
167        $returnValue = [
168            'itemData' => null
169        ];
170
171        if ($this->hasRequestParameter('uri')) {
172            $lang = taoItems_models_classes_ItemsService::singleton()->getSessionLg();
173            $itemUri = tao_helpers_Uri::decode($this->getRequestParameter('uri'));
174            $itemResource = new core_kernel_classes_Resource($itemUri);
175
176            // do not resolve xinclude here, leave it to the client side
177            $item = Service::singleton()->getDataItemByRdfItem($itemResource, $lang, false);
178
179            $returnValue['itemData'] = $item
180                ? $item->toArray()
181                : ['identifier' => $this->getItemIdentifier($itemResource)];
182
183            $availableLangs = \tao_helpers_I18n::getAvailableLangsByUsage(
184                new core_kernel_classes_Resource(TaoOntology::PROPERTY_STANCE_LANGUAGE_USAGE_DATA)
185            );
186            $returnValue['languagesList'] = $availableLangs;
187        }
188
189        $this->returnJson($returnValue);
190    }
191
192    public function saveItem()
193    {
194        $returnValue = ['success' => false];
195        $request = $this->getPsrRequest();
196        $queryParams = $request->getQueryParams();
197        if (isset($queryParams['uri'])) {
198            $xml = $request->getBody()->getContents();
199            $rdfItem = $this->getResource(urldecode($queryParams['uri']));
200            /** @var Service $itemService */
201            $itemService = $this->getServiceLocator()->get(Service::class);
202
203            if ($itemService->hasItemModel($rdfItem, [ItemModel::MODEL_URI])) {
204                try {
205                    $this->validateXmlInput($xml);
206                    Authoring::checkEmptyMedia($xml);
207
208                    $item = $this->getXmlToItemParser()->parseAndSanitize($xml);
209                    $this->getItemIdentifierValidator()->validate($item);
210
211                    $returnValue['success'] = $itemService->saveDataItemToRdfItem($item, $rdfItem);
212
213                    $this->getEventManager()->trigger(new ResourceUpdated($rdfItem));
214                    $this->getUpdatedItemEventDispatcher()->dispatch($item, $rdfItem);
215                } catch (QtiModelException $e) {
216                    $this->logError($e->getMessage());
217                    $returnValue = [
218                        'success' => false,
219                        'type' => 'Error',
220                        'message' => $e->getUserMessage()
221                    ];
222                } catch (common_exception_Error $e) {
223                    $this->logError(sprintf('Item XML is not valid: %s', $e->getMessage()));
224                    $returnValue = [
225                        'success' => false,
226                        'type' => 'Error',
227                        'message' => 'Item XML is not valid'
228                    ];
229                }
230            }
231        }
232
233        $this->returnJson($returnValue);
234    }
235
236    public function getFile()
237    {
238
239        if (
240            $this->hasRequestParameter('uri')
241            && $this->hasRequestParameter('lang')
242            && $this->hasRequestParameter('relPath')
243        ) {
244            $uri = urldecode($this->getRequestParameter('uri'));
245            $rdfItem = new core_kernel_classes_Resource($uri);
246
247            $lang = urldecode($this->getRequestParameter('lang'));
248
249            $rawParams = $this->getRequest()->getRawParameters();
250            $relPath   = ltrim(rawurldecode($rawParams['relPath']), '/');
251
252            $this->renderFile($rdfItem, $relPath, $lang);
253        }
254    }
255
256    private function renderFile($item, $path, $lang)
257    {
258        if (tao_helpers_File::securityCheck($path, true)) {
259            $resolver = new ItemMediaResolver($item, $lang);
260            $asset = $resolver->resolve($path);
261            $mediaSource = $asset->getMediaSource();
262            $stream = $mediaSource->getFileStream($asset->getMediaIdentifier());
263            $info = $mediaSource->getFileInfo($asset->getMediaIdentifier());
264            tao_helpers_Http::returnStream($stream, $info['mime']);
265        } else {
266            throw new common_exception_Error('invalid item preview file path');
267        }
268    }
269
270    /**
271     * Get the configuration of the Item Creator
272     * @param core_kernel_classes_Resource $item the selected item
273     * @return CreatorConfig the configration
274     */
275    protected function getCreatorConfig(core_kernel_classes_Resource $item)
276    {
277
278        $config = new CreatorConfig();
279
280        $creatorConfig = $this->getFeatureFlagConfigSwitcher()->getSwitchedExtensionConfig('taoQtiItem', 'qtiCreator');
281
282        if (is_array($creatorConfig)) {
283            foreach ($creatorConfig as $prop => $value) {
284                $config->setProperty($prop, $value);
285            }
286        }
287
288        $config->setProperty('uri', $item->getUri());
289        $config->setProperty('label', $item->getLabel());
290
291        //set the current data lang in the item content to keep the integrity
292        //@todo : allow preview in a language other than the one in the session
293        $lang = \common_session_SessionManager::getSession()->getDataLanguage();
294        $config->setProperty('lang', $lang);
295
296        // Add support for translation and side-by-side view
297        $config->setProperty('translation', $this->getRequestParameter('translation'));
298        $config->setProperty('originResourceUri', $this->getRequestParameter('originResourceUri'));
299
300        //base url:
301        $url = tao_helpers_Uri::url('getFile', 'QtiCreator', 'taoQtiItem', [
302            'uri' => $item->getUri(),
303            'lang' => $lang,
304            'relPath' => ''
305        ]);
306        $config->setProperty('baseUrl', $url);
307
308        //map the multi column config to the plugin
309        //TODO migrate the config
310        if ($config->getProperty('multi-column') == true) {
311            $config->addPlugin('blockAdder', 'taoQtiItem/qtiCreator/plugins/content/blockAdder', 'content');
312        }
313
314        if (
315            $config->getProperty('scrollable-multi-column') === true ||
316            $config->getProperty('separator-between-columns') === true
317        ) {
318            $config->addPlugin('layoutEditor', 'taoQtiItem/qtiCreator/plugins/panel/layoutEditor', 'panel');
319        }
320
321        $mediaSourcesUrl = tao_helpers_Uri::url(
322            'getMediaSources',
323            'QtiCreator',
324            'taoQtiItem'
325        );
326
327        $config->setProperty('mediaSourcesUrl', $mediaSourcesUrl);
328
329        //initialize all registered hooks:
330        $hookClasses = HookRegistry::getRegistry()->getMap();
331        foreach ($hookClasses as $hookClass) {
332            $hook = new $hookClass();
333            $hook->init($config);
334        }
335
336        $config->init();
337
338        return $config;
339    }
340
341    private function getUpdatedItemEventDispatcher(): UpdatedItemEventDispatcher
342    {
343        return $this->getServiceLocator()->get(UpdatedItemEventDispatcher::class);
344    }
345
346    private function getXmlToItemParser(): XmlToItemParser
347    {
348        return $this->getServiceLocator()->get(XmlToItemParser::class);
349    }
350
351    private function getItemIdentifierValidator(): ItemIdentifierValidator
352    {
353        return $this->getServiceLocator()->getContainer()->get(ItemIdentifierValidator::class);
354    }
355
356    private function getFeatureFlagConfigSwitcher(): FeatureFlagConfigSwitcher
357    {
358        return $this->getServiceLocator()->getContainer()->get(FeatureFlagConfigSwitcher::class);
359    }
360
361    /**
362     * Check if given string is a valid xml. Throw common_exception_Error if not.
363     *
364     * @param string $xml
365     * @throws common_exception_Error
366     */
367    private function validateXmlInput(string $xml)
368    {
369        if (trim($xml) === '') {
370            throw new common_exception_Error('Empty string given');
371        }
372        \tao_helpers_Xml::getSimpleXml($xml);
373    }
374
375    private function getItemIdentifier(core_kernel_classes_Resource $item): ?string
376    {
377        if ($this->getFeatureFlagChecker()->isEnabled('FEATURE_FLAG_UNIQUE_NUMERIC_QTI_IDENTIFIER')) {
378            $uniqueId = $item->getOnePropertyValue($this->getProperty(TaoOntology::PROPERTY_UNIQUE_IDENTIFIER));
379
380            if (!empty($uniqueId)) {
381                return $uniqueId;
382            }
383        }
384
385        return $this->getIdentifierGenerator()->generate([IdentifierGeneratorInterface::OPTION_RESOURCE => $item]);
386    }
387
388    private function getIdentifierGenerator(): IdentifierGeneratorInterface
389    {
390        return $this->getServiceLocator()->getContainer()->get(IdentifierGeneratorProxy::class);
391    }
392
393    private function getFeatureFlagChecker(): FeatureFlagCheckerInterface
394    {
395        return $this->getServiceLocator()->getContainer()->get(FeatureFlagChecker::class);
396    }
397}