Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
RestQtiItem
0.00% covered (danger)
0.00%
0 / 133
0.00% covered (danger)
0.00%
0 / 11
1190
0.00% covered (danger)
0.00%
0 / 1
 getAcceptableMimeTypes
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 getDestinationClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 index
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 import
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
72
 getTaskName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 importDeferred
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
20
 addExtraReturnData
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getUploadedPackage
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 createQtiItem
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 export
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 createClass
0.00% covered (danger)
0.00%
0 / 14
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) 2017-2025 (original work) Open Assessment Technologies SA;
19 *
20 */
21
22namespace oat\taoQtiItem\controller;
23
24use oat\tao\model\TaoOntology;
25use oat\tao\model\taskQueue\TaskLog\Entity\EntityInterface;
26use oat\tao\model\taskQueue\TaskLogInterface;
27use Request;
28use oat\taoQtiItem\model\qti\ImportService;
29use oat\taoQtiItem\model\ItemModel;
30use oat\generis\model\OntologyAwareTrait;
31use oat\oatbox\reporting\ReportInterface;
32use oat\taoQtiItem\model\qti\exception\ExtractException;
33use oat\taoQtiItem\model\qti\exception\ParsingException;
34use oat\taoQtiItem\model\Export\QTIPackedItemExporter;
35use oat\taoQtiItem\model\tasks\ImportQtiItem;
36use helpers_TimeOutHelper;
37use tao_helpers_Http;
38use tao_helpers_File;
39use ZipArchive;
40use tao_helpers_Export;
41use common_report_Report;
42use core_kernel_classes_Class;
43use common_exception_NotImplemented;
44use taoItems_models_classes_ItemsService;
45use common_Exception;
46use common_exception_BadRequest;
47use common_exception_ClassAlreadyExists;
48use common_exception_NotFound;
49use Exception;
50use common_exception_MissingParameter;
51
52/**
53 * End point of Rest item API
54 *
55 * @author Absar Gilani, <absar.gilani6@gmail.com>
56 * @author Gyula Szucs <gyula@taotesting.com>
57 */
58class RestQtiItem extends AbstractRestQti
59{
60    use OntologyAwareTrait;
61
62    public const RESTITEM_PACKAGE_NAME = 'content';
63
64    /**
65     * @inherit
66     */
67    protected function getAcceptableMimeTypes(): array
68    {
69        return
70            [
71                "application/json",
72                "text/xml",
73                "application/xml",
74                "application/rdf+xml" ,
75                "application/zip",
76            ];
77    }
78
79    /**
80     * Class items will be created in
81     */
82    protected function getDestinationClass(): core_kernel_classes_Class
83    {
84        return $this->getClassFromRequest($this->getClass(TaoOntology::CLASS_URI_ITEM));
85    }
86
87    /**
88     * Only import method is available, so index return failure response
89     */
90    public function index(): void
91    {
92        $this->returnFailure(new common_exception_NotImplemented('This API does not support this call.'));
93    }
94
95    /**
96     * Import file entry point by using $this->service
97     * Check POST method & get valid uploaded file
98     */
99    public function import(): void
100    {
101        try {
102            if ($this->getRequestMethod() != Request::HTTP_POST) {
103                throw new common_exception_NotImplemented('Only post method is accepted to import Qti package.');
104            }
105
106            // Get valid package parameter
107            $package = $this->getUploadedPackage();
108
109            // Call service to import package
110            helpers_TimeOutHelper::setTimeOutLimit(helpers_TimeOutHelper::LONG);
111            $report = ImportService::singleton()->importQTIPACKFile(
112                $package,
113                $this->getDestinationClass(),
114                true,
115                true,
116                true,
117                $this->isMetadataGuardiansEnabled(),
118                $this->isMetadataValidatorsEnabled(),
119                $this->isItemMustExistEnabled(),
120                $this->isItemMustBeOverwrittenEnabled(),
121                $this->isMetadataRequired()
122            );
123            helpers_TimeOutHelper::reset();
124
125            tao_helpers_File::remove($package);
126            if ($report->getType() !== ReportInterface::TYPE_SUCCESS) {
127                $message = __("An unexpected error occurred during the import of the IMS QTI Item Package. ");
128                //get message of first error report
129                if (!empty($report->getErrors())) {
130                    $message .= $report->getErrors()[0]->getMessage();
131                }
132                $this->returnFailure(new common_Exception($message));
133            } else {
134                $itemIds = [];
135                /** @var common_report_Report $subReport */
136                foreach ($report as $subReport) {
137                    $itemIds[] = $subReport->getData()->getUri();
138                }
139                $this->setSuccessJsonResponse(['items' => $itemIds]);
140            }
141        } catch (ExtractException $e) {
142            $this->returnFailure(
143                new common_Exception(
144                    __('The ZIP archive containing the IMS QTI Item cannot be extracted.')
145                )
146            );
147        } catch (ParsingException $e) {
148            $this->returnFailure(
149                new common_Exception(
150                    __('The ZIP archive does not contain an imsmanifest.xml file or is an invalid ZIP archive.')
151                )
152            );
153        } catch (Exception $e) {
154            $this->returnFailure($e);
155        }
156    }
157
158    /**
159     * @inheritdoc
160     */
161    protected function getTaskName(): string
162    {
163        return ImportQtiItem::class;
164    }
165
166    /**
167     * Import item package through the task queue.
168     */
169    public function importDeferred(): void
170    {
171        try {
172            if ($this->getRequestMethod() != Request::HTTP_POST) {
173                throw new common_exception_NotImplemented('Only post method is accepted to import Qti package.');
174            }
175
176            $task = ImportQtiItem::createTask(
177                $this->getUploadedPackage(),
178                $this->getDestinationClass(),
179                $this->getServiceLocator(),
180                $this->isMetadataGuardiansEnabled(),
181                $this->isMetadataValidatorsEnabled(),
182                $this->isItemMustExistEnabled(),
183                $this->isItemMustBeOverwrittenEnabled(),
184                $this->isMetadataRequired()
185            );
186
187            $result = [
188                'reference_id' => $task->getId()
189            ];
190
191            /** @var TaskLogInterface $taskLog */
192            $taskLog = $this->getServiceManager()->get(TaskLogInterface::SERVICE_ID);
193
194            if ($report = $taskLog->getReport($task->getId())) {
195                $result['report'] = $report->toArray();
196            }
197
198            $this->setSuccessJsonResponse($result);
199        } catch (Exception $e) {
200            $this->returnFailure($e);
201        }
202    }
203
204    /**
205     * Add extra values to the JSON returned.
206     *
207     * @param EntityInterface $taskLogEntity
208     */
209    protected function addExtraReturnData(EntityInterface $taskLogEntity): array
210    {
211        $data = [];
212
213        if ($taskLogEntity->getReport()) {
214            $plainReport = $this->getPlainReport($taskLogEntity->getReport());
215
216            //the third report is report of import test
217            $itemsReport = array_slice($plainReport, 2);
218            foreach ($itemsReport as $itemReport) {
219                if (isset($itemReport->getData()['uriResource'])) {
220                    $data['itemIds'][] = $itemReport->getData()['uriResource'];
221                }
222            }
223        }
224
225        return $data;
226    }
227
228    /**
229     * Return a valid uploaded file
230     */
231    protected function getUploadedPackage(): string
232    {
233        if (!$this->getQueryParams(self::RESTITEM_PACKAGE_NAME)) {
234            throw new common_exception_MissingParameter(self::RESTITEM_PACKAGE_NAME, __CLASS__);
235        }
236
237        $file = tao_helpers_Http::getUploadedFile(self::RESTITEM_PACKAGE_NAME);
238
239        if (!in_array($file['type'], self::$accepted_types)) {
240            throw new common_exception_BadRequest('Uploaded file has to be a valid archive.');
241        }
242
243        $pathinfo = pathinfo($file['tmp_name']);
244        $destination = $pathinfo['dirname'] . DIRECTORY_SEPARATOR . $file['name'];
245        tao_helpers_File::move($file['tmp_name'], $destination);
246
247        return $destination;
248    }
249
250    /**
251     * Create an empty item
252     */
253    public function createQtiItem(): void
254    {
255        try {
256            // Check if it's post method
257            if ($this->getRequestMethod() != Request::HTTP_POST) {
258                throw new common_exception_NotImplemented('Only post method is accepted to create empty item.');
259            }
260
261            $label = $this->getQueryParams('label') ?? '';
262            // Call service to import package
263            $item = $this->getDestinationClass()->createInstance($label);
264
265            /** @var taoItems_models_classes_ItemsService $itemService */
266            $itemService = $this->getServiceLocator()->get(taoItems_models_classes_ItemsService::class);
267            $itemService->setItemModel($item, $this->getResource(ItemModel::MODEL_URI));
268
269            $this->setSuccessJsonResponse($item->getUri());
270        } catch (Exception $e) {
271            $this->returnFailure($e);
272        }
273    }
274
275    /**
276     * render an item as a Qti zip package
277     * @author christophe GARCIA <christopheg@taotesting.com>
278     */
279    public function export()
280    {
281
282        try {
283            if ($this->getRequestMethod() != Request::HTTP_GET) {
284                throw new common_exception_NotImplemented('Only GET method is accepted to export QIT Item.');
285            }
286
287            if (!$this->getQueryParams('id')) {
288                $this->returnFailure(new common_exception_MissingParameter('required parameter `id` is missing'));
289            }
290
291            $id = $this->getQueryParams('id');
292
293            $item = $this->getResource($id);
294
295            /** @var taoItems_models_classes_ItemsService $itemService */
296            $itemService = $this->getServiceLocator()->get(taoItems_models_classes_ItemsService::class);
297
298            if ($itemService->hasItemModel($item, [ItemModel::MODEL_URI])) {
299                $path = tao_helpers_Export::getExportFile();
300                $tmpZip = new ZipArchive();
301                $tmpZip->open($path, ZipArchive::CREATE);
302
303                $exporter = new QTIPackedItemExporter($item, $tmpZip);
304                $exporter->export(['apip' => false]);
305
306                $exporter->getZip()->close();
307
308                header('Content-Type: application/zip');
309                tao_helpers_Http::returnFile($path, false);
310
311                return;
312            } else {
313                $this->returnFailure(new common_exception_NotFound('item can\'t be found'));
314            }
315        } catch (Exception $e) {
316            $this->returnFailure($e);
317        }
318    }
319
320    /**
321     * Create an Item Class
322     *
323     * Label parameter is mandatory
324     * If parent class parameter is an uri of valid test class, new class will be created under it
325     * If not parent class parameter is provided, class will be created under root class
326     * Comment parameter is not mandatory, used to describe new created class
327     *
328     */
329    public function createClass(): void
330    {
331        try {
332            $class = $this->createSubClass($this->getClass(TaoOntology::CLASS_URI_ITEM));
333
334            $result = [
335                'message' => __('Class successfully created.'),
336                'class-uri' => $class->getUri(),
337            ];
338
339            $this->setSuccessJsonResponse($result);
340        } catch (common_exception_ClassAlreadyExists $e) {
341            $result = [
342                'message' => $e->getMessage(),
343                'class-uri' => $e->getClass()->getUri(),
344            ];
345            $this->setSuccessJsonResponse($result);
346        } catch (\Exception $e) {
347            $this->returnFailure($e);
348        }
349    }
350}