Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 146
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
QtiItemCompiler
0.00% covered (danger)
0.00%
0 / 146
0.00% covered (danger)
0.00%
0 / 6
812
0.00% covered (danger)
0.00%
0 / 1
 compile
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 internalCompile
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
20
 createQtiService
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
2
 deployQtiItem
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
30
 retrieveAssets
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
182
 compileItemIndex
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
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 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 *
20 */
21
22namespace oat\taoQtiItem\model;
23
24use common_report_Report;
25use core_kernel_classes_Resource;
26use oat\oatbox\filesystem\Directory;
27use oat\taoItems\model\ItemCompilerIndex;
28use oat\taoQtiItem\model\compile\QtiItemCompilerAssetBlacklist;
29use oat\taoQtiItem\model\qti\exception\XIncludeException;
30use oat\taoQtiItem\model\qti\Item;
31use oat\taoQtiItem\model\qti\Service;
32use tao_models_classes_service_ConstantParameter;
33use tao_models_classes_service_ServiceCall;
34use tao_models_classes_service_StorageDirectory;
35use taoItems_models_classes_CompilationFailedException;
36use taoItems_models_classes_ItemCompiler;
37use taoItems_models_classes_ItemsService;
38use oat\taoQtiItem\model\qti\Parser;
39use oat\taoQtiItem\model\qti\AssetParser;
40use oat\taoQtiItem\model\qti\XIncludeLoader;
41use oat\taoItems\model\media\ItemMediaResolver;
42
43/**
44 * The QTI Item Compiler
45 *
46 * @access public
47 * @author Joel Bout, <joel@taotesting.com>
48 * @package taoItems
49 */
50class QtiItemCompiler extends taoItems_models_classes_ItemCompiler
51{
52    /**
53     * instance representing the service to run the QTI item
54     * @var string
55     */
56    public const INSTANCE_ITEMRUNNER = 'http://www.tao.lu/Ontologies/TAOItem.rdf#ServiceQtiItemRunner';
57
58    /**
59     * {@inheritDoc}
60     * @see \tao_models_classes_Compiler::compile()
61     */
62    public function compile()
63    {
64        $report = $this->internalCompile();
65        if ($report->getType() == common_report_Report::TYPE_SUCCESS) {
66            // replace instances with service
67            list($item, $publicDirectory, $privateDirectory) = $report->getData();
68            $report->setData($this->createQtiService($item, $publicDirectory, $privateDirectory));
69        }
70        return $report;
71    }
72
73    /**
74     * Compile qti item
75     *
76     * @throws taoItems_models_classes_CompilationFailedException
77     * @return common_report_Report
78     */
79    protected function internalCompile()
80    {
81        $item = $this->getResource();
82        $publicDirectory = $this->spawnPublicDirectory();
83        $privateDirectory = $this->spawnPrivateDirectory();
84
85        $report = new common_report_Report(common_report_Report::TYPE_SUCCESS, __('Published %s', $item->getLabel()));
86        $report->setData([$item, $publicDirectory, $privateDirectory]);
87        $langs = $this->getContentUsedLanguages();
88        if (empty($langs)) {
89            $report->setType(common_report_Report::TYPE_ERROR);
90            $report->setMessage(__('Item "%s" is not available in any language', $item->getLabel()));
91        }
92        foreach ($langs as $compilationLanguage) {
93            $langReport = $this->deployQtiItem($item, $compilationLanguage, $publicDirectory, $privateDirectory);
94            $report->add($langReport);
95            if ($langReport->getType() == common_report_Report::TYPE_ERROR) {
96                $report->setType(common_report_Report::TYPE_ERROR);
97                $report->setMessage(__('Failed to publish %1$s in %2$s', $item->getLabel(), $compilationLanguage));
98                break;
99            }
100        }
101        return $report;
102    }
103
104    /**
105     * Create a servicecall that runs the prepared qti item
106     *
107     * @param core_kernel_classes_Resource $item
108     * @param tao_models_classes_service_StorageDirectory $publicDirectory
109     * @param tao_models_classes_service_StorageDirectory $privateDirectory
110     * @return tao_models_classes_service_ServiceCall
111     */
112    protected function createQtiService(
113        core_kernel_classes_Resource $item,
114        tao_models_classes_service_StorageDirectory $publicDirectory,
115        tao_models_classes_service_StorageDirectory $privateDirectory
116    ) {
117
118        $service = new tao_models_classes_service_ServiceCall(
119            new core_kernel_classes_Resource(self::INSTANCE_ITEMRUNNER)
120        );
121        $service->addInParameter(new tao_models_classes_service_ConstantParameter(
122            new core_kernel_classes_Resource(taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_PATH),
123            $publicDirectory->getId()
124        ));
125        $service->addInParameter(
126            new tao_models_classes_service_ConstantParameter(
127                new core_kernel_classes_Resource(
128                    taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_DATA_PATH
129                ),
130                $privateDirectory->getId()
131            )
132        );
133        $service->addInParameter(
134            new tao_models_classes_service_ConstantParameter(
135                new core_kernel_classes_Resource(taoItems_models_classes_ItemsService::INSTANCE_FORMAL_PARAM_ITEM_URI),
136                $item
137            )
138        );
139
140        return $service;
141    }
142
143    /**
144     * Desploy all the required files into the provided directories
145     *
146     * @param core_kernel_classes_Resource $item
147     * @param string $language
148     * @param tao_models_classes_service_StorageDirectory $publicDirectory
149     * @param tao_models_classes_service_StorageDirectory $privateDirectory
150     * @return common_report_Report
151     */
152    protected function deployQtiItem(
153        core_kernel_classes_Resource $item,
154        $language,
155        tao_models_classes_service_StorageDirectory $publicDirectory,
156        tao_models_classes_service_StorageDirectory $privateDirectory
157    ) {
158        $itemService = taoItems_models_classes_ItemsService::singleton();
159        $qtiService = Service::singleton();
160
161        //copy item.xml file to private directory
162        $itemDir = $itemService->getItemDirectory($item, $language);
163
164        $sourceItem = $itemDir->getFile('qti.xml');
165        $privateDirectory->writeStream($language . '/qti.xml', $sourceItem->readStream());
166
167        //copy client side resources (javascript loader)
168        $qtiItemDir = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiItem')->getDir();
169        $taoDir = \common_ext_ExtensionsManager::singleton()->getExtensionById('tao')->getDir();
170        $assetPath = $qtiItemDir . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR;
171        $assetLibPath = $taoDir . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR
172            . 'lib' . DIRECTORY_SEPARATOR;
173
174        if (\tao_helpers_Mode::is('production')) {
175            $fh = fopen($assetPath . 'loader' . DIRECTORY_SEPARATOR . 'qtiLoader.min.js', 'r');
176            $publicDirectory->writeStream($language . '/qtiLoader.min.js', $fh);
177            fclose($fh);
178        } else {
179            $fh = fopen($assetPath . 'runtime' . DIRECTORY_SEPARATOR . 'qtiLoader.js', 'r');
180            $publicDirectory->writeStream($language . '/qtiLoader.js', $fh);
181            fclose($fh);
182            $fh = fopen($assetLibPath . 'require.js', 'r');
183            $publicDirectory->writeStream($language . '/require.js', $fh);
184            fclose($fh);
185        }
186
187        //  retrieve the media assets
188        try {
189            $qtiItem = $this->retrieveAssets($item, $language, $publicDirectory);
190            $this->compileItemIndex($item->getUri(), $qtiItem, $language);
191
192            //store variable qti elements data into the private directory
193            $variableElements = $qtiService->getVariableElements($qtiItem);
194
195            $stream = \GuzzleHttp\Psr7\stream_for(json_encode($variableElements));
196            $privateDirectory->writePsrStream($language . '/variableElements.json', $stream);
197            $stream->close();
198
199            // render item based on the modified QtiItem
200            $xhtml = $qtiService->renderQTIItem($qtiItem, $language);
201
202            //note : no need to manually copy qti or other third party lib files, all dependencies are managed
203            // by requirejs
204            // write index.html
205            $stream = \GuzzleHttp\Psr7\stream_for($xhtml);
206            $publicDirectory->writePsrStream($language . '/index.html', $stream, 'text/html');
207            $stream->close();
208
209            return new common_report_Report(
210                common_report_Report::TYPE_SUCCESS,
211                __('Successfully compiled "%s"', $language)
212            );
213        } catch (\tao_models_classes_FileNotFoundException $e) {
214            return new common_report_Report(
215                common_report_Report::TYPE_ERROR,
216                __('Unable to retrieve asset "%s"', $e->getFilePath())
217            );
218        } catch (XIncludeException $e) {
219            return new common_report_Report(
220                common_report_Report::TYPE_ERROR,
221                $e->getUserMessage()
222            );
223        } catch (\Exception $e) {
224            return new common_report_Report(
225                common_report_Report::TYPE_ERROR,
226                $e->getMessage()
227            );
228        }
229    }
230
231    /**
232     * @param core_kernel_classes_Resource $item
233     * @param string $lang
234     * @param Directory $publicDirectory
235     * @return qti\Item
236     * @throws taoItems_models_classes_CompilationFailedException
237     */
238    protected function retrieveAssets(core_kernel_classes_Resource $item, $lang, Directory $publicDirectory)
239    {
240        $qtiItem  = Service::singleton()->getDataItemByRdfItem($item, $lang);
241
242        if (is_null($qtiItem)) {
243            throw new taoItems_models_classes_CompilationFailedException(
244                __('Unable to retrieve item : ' . $item->getLabel())
245            );
246        }
247
248        $assetParser = new AssetParser($qtiItem, $publicDirectory);
249        $assetParser->setGetSharedLibraries(false);
250        $assetParser->setGetXinclude(false);
251        $resolver = new ItemMediaResolver($item, $lang);
252        $replacementList = [];
253        foreach ($assetParser->extract() as $type => $assets) {
254            foreach ($assets as $assetUrl) {
255
256                /** @var QtiItemCompilerAssetBlacklist $blacklistService */
257                $blacklistService = $this->getServiceLocator()->get(QtiItemCompilerAssetBlacklist::SERVICE_ID);
258                if ($blacklistService->isBlacklisted($assetUrl)) {
259                    continue;
260                }
261
262                $mediaAsset = $resolver->resolve($assetUrl);
263                $mediaSource = $mediaAsset->getMediaSource();
264
265                $basename = $mediaSource->getBaseName($mediaAsset->getMediaIdentifier());
266                $replacement = $basename;
267                $count = 0;
268                while (in_array($replacement, $replacementList)) {
269                    $dot = strrpos($basename, '.');
270                    $replacement = $dot !== false
271                        ? substr($basename, 0, $dot) . '_' . $count . substr($basename, $dot)
272                        : $basename . $count;
273                    $count++;
274                }
275                $replacementList[$assetUrl] = $replacement;
276                $tmpfile = $mediaSource->download($mediaAsset->getMediaIdentifier());
277                $fh = fopen($tmpfile, 'r');
278                $publicDirectory->writeStream($lang . '/' . $replacement, $fh);
279                fclose($fh);
280                unlink($tmpfile);
281
282                //$fileStream = $mediaSource->getFileStream($mediaAsset->getMediaIdentifier());
283                //$publicDirectory->writeStream($lang.'/'.$replacement, $fileStream);
284            }
285        }
286
287        $dom = new \DOMDocument('1.0', 'UTF-8');
288        if ($dom->loadXML($qtiItem->toXml()) === true) {
289            $xpath = new \DOMXPath($dom);
290            $attributeNodes = $xpath->query('//@*');
291            foreach ($attributeNodes as $node) {
292                if (isset($replacementList[$node->value])) {
293                    $node->value = $replacementList[$node->value];
294                }
295            }
296
297            //@TODO : Fix me please
298            $attributeNodes = $xpath->query("//*[local-name()='entry']|//*[local-name()='property']") ?: [];
299            unset($xpath);
300            foreach ($attributeNodes as $node) {
301                if ($node->nodeValue) {
302                    $node->nodeValue = strtr(htmlentities($node->nodeValue, ENT_XML1), $replacementList);
303                }
304            }
305        } else {
306            throw new taoItems_models_classes_CompilationFailedException('Unable to load XML');
307        }
308
309        $qtiParser = new Parser($dom->saveXML());
310        $assetRetrievedQtiItem =  $qtiParser->load();
311
312        //loadxinclude
313        $xincludeLoader = new XIncludeLoader($assetRetrievedQtiItem, $resolver);
314        $xincludeLoader->load(false);
315
316        return $assetRetrievedQtiItem;
317    }
318
319    /**
320     * @param string $uri
321     * @param Item $qtiItem
322     * @param $language
323     */
324    protected function compileItemIndex($uri, Item $qtiItem, $language)
325    {
326        $context = $this->getContext();
327        if ($context && $context instanceof ItemCompilerIndex) {
328            $context->setItem($uri, $language, $qtiItem->getAttributeValues());
329        }
330    }
331}