Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 157
0.00% covered (danger)
0.00%
0 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
QtiCreator
0.00% covered (danger)
0.00%
0 / 157
0.00% covered (danger)
0.00%
0 / 18
2256
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
 getCreatorConfigFactory
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 oat\taoQtiItem\model\service\CreatorConfigFactory;
52use tao_actions_CommonModule;
53use tao_helpers_File;
54use tao_helpers_Http;
55use tao_helpers_Uri;
56use taoItems_models_classes_ItemsService;
57
58/**
59 * QtiCreator Controller provide actions to edit a QTI item
60 *
61 * @author CRP Henri Tudor - TAO Team - {@link http://www.tao.lu}
62 * @package taoQTI
63 * @license GPLv2  http://www.opensource.org/licenses/gpl-2.0.php
64 */
65class QtiCreator extends tao_actions_CommonModule
66{
67    use OntologyAwareTrait;
68    use HttpJsonResponseTrait;
69
70    /**
71     * @return EventManager
72     */
73    protected function getEventManager()
74    {
75        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
76    }
77
78    /**
79     * create a new QTI item
80     *
81     * @throws common_exception_BadRequest
82     * @throws common_exception_Error
83     *
84     * @requiresRight id WRITE
85     * @requiresRight classUri WRITE
86     */
87    public function createItem()
88    {
89        if (!\tao_helpers_Request::isAjax()) {
90            throw new common_exception_BadRequest('wrong request mode');
91        }
92        try {
93            $this->validateCsrf();
94        } catch (\common_exception_Unauthorized $e) {
95            $this->response = $this->getPsrResponse()->withStatus(403, __('Unable to process your request'));
96            return;
97        }
98
99        $clazz = new \core_kernel_classes_Resource($this->getRequestParameter('id'));
100        if ($clazz->isClass()) {
101            $clazz = new \core_kernel_classes_Class($clazz);
102        } else {
103            foreach ($clazz->getTypes() as $type) {
104                // determine class from selected instance
105                $clazz = $type;
106                break;
107            }
108        }
109        $service = \taoItems_models_classes_ItemsService::singleton();
110
111        $label = $service->createUniqueLabel($clazz);
112        $item = $service->createInstance($clazz, $label);
113
114        if ($item !== null) {
115            $service->setItemModel($item, new \core_kernel_classes_Resource(ItemModel::MODEL_URI));
116            $this->getEventManager()->trigger(new ItemCreatedEvent($item->getUri()));
117            $response = [
118                'success' => true,
119                'message' => __('Successfully created item "%s"', $item->getLabel()),
120                'label'   => $item->getLabel(),
121                'uri'     => $item->getUri()
122            ];
123        } else {
124            $response = false;
125        }
126        $this->returnJson($response);
127    }
128
129    public function index()
130    {
131
132        if (!$this->hasRequestParameter('instance')) {
133            throw new common_exception_Error('The item creator needs to be opened with an item');
134        }
135        $item = new core_kernel_classes_Resource(tao_helpers_Uri::decode($this->getRequestParameter('instance')));
136
137        $config = $this->getCreatorConfig($item);
138
139        $this->setData('config', $config->toArray());
140        $this->setView('QtiCreator/index.tpl');
141
142        $this->getEventManager()->trigger(new ItemCreatorLoad());
143    }
144
145    public function getMediaSources()
146    {
147        $exclude = '';
148        if ($this->hasRequestParameter('exclude')) {
149            $exclude = $this->getRequestParameter('exclude');
150        }
151        // get the config media Sources
152        $sources = array_keys(MediaService::singleton()->getBrowsableSources());
153        $mediaSources = [];
154        if ($exclude !== 'local') {
155            $mediaSources[] = ['root' => 'local', 'path' => '/'];
156        }
157        foreach ($sources as $source) {
158            if ($source !== $exclude) {
159                $mediaSources[] = ['root' => $source, 'path' => 'taomedia://' . $source . '/'];
160            }
161        }
162
163        $this->returnJson($mediaSources);
164    }
165
166    public function getItemData()
167    {
168        $returnValue = [
169            'itemData' => null
170        ];
171
172        if ($this->hasRequestParameter('uri')) {
173            $lang = taoItems_models_classes_ItemsService::singleton()->getSessionLg();
174            $itemUri = tao_helpers_Uri::decode($this->getRequestParameter('uri'));
175            $itemResource = new core_kernel_classes_Resource($itemUri);
176
177            // do not resolve xinclude here, leave it to the client side
178            $item = Service::singleton()->getDataItemByRdfItem($itemResource, $lang, false);
179
180            $returnValue['itemData'] = $item
181                ? $item->toArray()
182                : ['identifier' => $this->getItemIdentifier($itemResource)];
183
184            $availableLangs = \tao_helpers_I18n::getAvailableLangsByUsage(
185                new core_kernel_classes_Resource(TaoOntology::PROPERTY_STANCE_LANGUAGE_USAGE_DATA)
186            );
187            $returnValue['languagesList'] = $availableLangs;
188        }
189
190        $this->returnJson($returnValue);
191    }
192
193    public function saveItem()
194    {
195        $returnValue = ['success' => false];
196        $request = $this->getPsrRequest();
197        $queryParams = $request->getQueryParams();
198        if (isset($queryParams['uri'])) {
199            $xml = $request->getBody()->getContents();
200            $rdfItem = $this->getResource(urldecode($queryParams['uri']));
201            /** @var Service $itemService */
202            $itemService = $this->getServiceLocator()->get(Service::class);
203
204            if ($itemService->hasItemModel($rdfItem, [ItemModel::MODEL_URI])) {
205                try {
206                    $this->validateXmlInput($xml);
207                    Authoring::checkEmptyMedia($xml);
208
209                    $item = $this->getXmlToItemParser()->parseAndSanitize($xml);
210                    $this->getItemIdentifierValidator()->validate($item);
211
212                    $returnValue['success'] = $itemService->saveDataItemToRdfItem($item, $rdfItem);
213
214                    $this->getEventManager()->trigger(new ResourceUpdated($rdfItem));
215                    $this->getUpdatedItemEventDispatcher()->dispatch($item, $rdfItem);
216                } catch (QtiModelException $e) {
217                    $this->logError($e->getMessage());
218                    $returnValue = [
219                        'success' => false,
220                        'type' => 'Error',
221                        'message' => $e->getUserMessage()
222                    ];
223                } catch (common_exception_Error $e) {
224                    $this->logError(sprintf('Item XML is not valid: %s', $e->getMessage()));
225                    $returnValue = [
226                        'success' => false,
227                        'type' => 'Error',
228                        'message' => 'Item XML is not valid'
229                    ];
230                }
231            }
232        }
233
234        $this->returnJson($returnValue);
235    }
236
237    public function getFile()
238    {
239
240        if (
241            $this->hasRequestParameter('uri')
242            && $this->hasRequestParameter('lang')
243            && $this->hasRequestParameter('relPath')
244        ) {
245            $uri = urldecode($this->getRequestParameter('uri'));
246            $rdfItem = new core_kernel_classes_Resource($uri);
247
248            $lang = urldecode($this->getRequestParameter('lang'));
249
250            $rawParams = $this->getRequest()->getRawParameters();
251            $relPath   = ltrim(rawurldecode($rawParams['relPath']), '/');
252
253            $this->renderFile($rdfItem, $relPath, $lang);
254        }
255    }
256
257    private function renderFile($item, $path, $lang)
258    {
259        if (tao_helpers_File::securityCheck($path, true)) {
260            $resolver = new ItemMediaResolver($item, $lang);
261            $asset = $resolver->resolve($path);
262            $mediaSource = $asset->getMediaSource();
263            $stream = $mediaSource->getFileStream($asset->getMediaIdentifier());
264            $info = $mediaSource->getFileInfo($asset->getMediaIdentifier());
265            tao_helpers_Http::returnStream($stream, $info['mime']);
266        } else {
267            throw new common_exception_Error('invalid item preview file path');
268        }
269    }
270
271    /**
272     * Get the configuration of the Item Creator
273     * @param core_kernel_classes_Resource $item the selected item
274     * @return CreatorConfig the configration
275     */
276    protected function getCreatorConfig(core_kernel_classes_Resource $item)
277    {
278        $config = $this->getCreatorConfigFactory()->getCreatorConfig();
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
398    private function getCreatorConfigFactory(): CreatorConfigFactory
399    {
400        return $this->getPsrContainer()->get(CreatorConfigFactory::class);
401    }
402}