Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 356
0.00% covered (danger)
0.00%
0 / 38
CRAP
0.00% covered (danger)
0.00%
0 / 1
taoQtiTest_actions_TestRunner
0.00% covered (danger)
0.00%
0 / 356
0.00% covered (danger)
0.00%
0 / 38
14280
0.00% covered (danger)
0.00%
0 / 1
 getTestSession
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTestSession
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestDefinition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTestDefinition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStorage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setStorage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPreviousError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setCurrentError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCurrentError
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setCompilationDirectory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCompilationDirectory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setTestMeta
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestMeta
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setItemIndex
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 notifyError
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 beforeAction
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
42
 getMetaDataHandler
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 afterAction
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
6
 index
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 keepItemTimed
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 markForReview
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
42
 jumpTo
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 endTimedSection
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
132
 moveForward
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 moveBackward
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 nextSection
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 skip
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 timeout
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 endTestSession
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 onTimeout
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 storeItemVariableSet
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
132
 comment
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
6
 retrieveTestDefinition
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 retrieveTestSession
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 retrieveTestMeta
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 retrieveItemIndex
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 handleAssessmentTestSessionException
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
42
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 */
22
23use oat\taoDelivery\model\execution\DeliveryServerService;
24use qtism\runtime\tests\AssessmentTestSessionException;
25use qtism\runtime\tests\AssessmentTestSessionState;
26use qtism\runtime\tests\AssessmentTestSession;
27use qtism\data\AssessmentTest;
28use qtism\runtime\common\State;
29use qtism\runtime\common\ResponseVariable;
30use qtism\common\enums\BaseType;
31use qtism\common\enums\Cardinality;
32use qtism\common\datatypes\QtiString as QtismString;
33use qtism\runtime\storage\binary\BinaryAssessmentTestSeeker;
34use qtism\runtime\storage\common\AbstractStorage;
35use qtism\data\SubmissionMode;
36use qtism\data\NavigationMode;
37use oat\taoQtiItem\helpers\QtiRunner;
38use oat\taoQtiTest\models\TestSessionMetaData;
39use oat\taoQtiTest\models\QtiTestCompilerIndex;
40use oat\taoQtiTest\models\files\QtiFlysystemFileManager;
41use oat\taoQtiTest\models\runner\StorageManager;
42use oat\oatbox\service\ServiceManager;
43use oat\taoQtiTest\models\CompilationDataService;
44
45/**
46 * Runs a QTI Test.
47 *
48 * @author Joel Bout <joel@taotesting.com>
49 * @author Jérôme Bogaerts <jerome@taotesting.com>
50 * @package taoQtiTest
51 * @deprecated old testrunner is deprecated. use taoQtiTest_actions_Runner instead
52 * @license GPLv2  http://www.opensource.org/licenses/gpl-2.0.php
53 */
54class taoQtiTest_actions_TestRunner extends tao_actions_ServiceModule
55{
56    /**
57     * The current AssessmentTestSession object.
58     *
59     * @var AssessmentTestSession
60     */
61    private $testSession = null;
62
63    /**
64     * The current AssessmentTest definition object.
65     *
66     * @var AssessmentTest
67     */
68    private $testDefinition = null;
69
70    /**
71     * The current AbstractStorage object.
72     *
73     * @var AbstractStorage
74     */
75    private $storage = null;
76
77    /**
78     * The error that occured during the current request.
79     *
80     */
81    private $currentError = -1;
82
83    /**
84     * The compilation directory.
85     *
86     * @var string
87     */
88    private $compilationDirectory;
89
90    /**
91     * The meta data about the test definition
92     * being executed.
93     *
94     * @var array
95     */
96    private $testMeta;
97
98    /**
99     * The index of compiled items.
100     *
101     * @var QtiTestCompilerIndex
102     */
103    private $itemIndex;
104
105    /**
106     * Testr session metadata manager
107     *
108     * @var TestSessionMetaData
109     */
110    private $metaDataHandler;
111
112    /**
113     * Get the current assessment test session.
114     *
115     * @return AssessmentTestSession An AssessmentTestSession object.
116     */
117    protected function getTestSession()
118    {
119        return $this->testSession;
120    }
121
122    /**
123     * Set the current assessment test session.
124     *
125     * @param AssessmentTestSession $testSession An AssessmentTestSession object.
126     */
127    protected function setTestSession(AssessmentTestSession $testSession)
128    {
129        $this->testSession = $testSession;
130    }
131
132    /**
133     * Get the current test definition.
134     *
135     * @return AssessmentTest An AssessmentTest object.
136     */
137    protected function getTestDefinition()
138    {
139        return $this->testDefinition;
140    }
141
142    /**
143     * Set the current test defintion.
144     *
145     * @param AssessmentTest $testDefinition An AssessmentTest object.
146     */
147    protected function setTestDefinition(AssessmentTest $testDefinition)
148    {
149        $this->testDefinition = $testDefinition;
150    }
151
152    /**
153     * Get the QtiSm AssessmentTestSession Storage Service.
154     *
155     * @return AbstractStorage An AssessmentTestSession Storage Service.
156     */
157    protected function getStorage()
158    {
159        return $this->storage;
160    }
161
162    /**
163     * Set the QtiSm AssessmentTestSession Storage Service.
164     *
165     * @param AbstractStorage $storage An AssessmentTestSession Storage Service.
166     */
167    protected function setStorage(AbstractStorage $storage)
168    {
169        $this->storage = $storage;
170    }
171
172    /**
173     * Get the error that occured during the previous request.
174     *
175     * @return integer
176     */
177    protected function getPreviousError()
178    {
179        return $this->getStorage()->getLastError();
180    }
181
182    /**
183     * Set the error that occured during the current request.
184     *
185     * @param integer $error
186     */
187    protected function setCurrentError($currentError)
188    {
189        $this->currentError = $currentError;
190    }
191
192    /**
193     * Get the error that occured during the current request.
194     *
195     * @return integer
196     */
197    protected function getCurrentError()
198    {
199        return $this->currentError;
200    }
201
202    /**
203     * Set the path to the directory where the test is compiled.
204     *
205     * @param string $compilationDirectory An absolute path.
206     */
207    protected function setCompilationDirectory($compilationDirectory)
208    {
209        $this->compilationDirectory = $compilationDirectory;
210    }
211
212    /**
213     * Get the path to the directory where the test is compiled.
214     *
215     * @return tao_models_classes_service_StorageDirectory
216     */
217    protected function getCompilationDirectory()
218    {
219        return $this->compilationDirectory;
220    }
221
222    /**
223     * Set the meta-data array about the test definition
224     * being executed.
225     *
226     * @param array $testMeta
227     */
228    protected function setTestMeta(array $testMeta)
229    {
230        $this->testMeta = $testMeta;
231    }
232
233    /**
234     * Get the meta-data array about the test definition
235     * being executed.
236     *
237     * @return array
238     */
239    protected function getTestMeta()
240    {
241        return $this->testMeta;
242    }
243
244    /**
245     * @return QtiTestCompilerIndex
246     */
247    protected function getItemIndex()
248    {
249        return $this->itemIndex;
250    }
251
252    /**
253     * @param QtiTestCompilerIndex $itemIndex
254     * @return taoQtiTest_actions_TestRunner
255     */
256    protected function setItemIndex($itemIndex)
257    {
258        $this->itemIndex = $itemIndex;
259        return $this;
260    }
261
262    /**
263     * Print an error report into the response.
264     * After you have called this method, you must prevent other actions to be processed and must close the response.
265     * @param string $message
266     * @param int $code
267     */
268    protected function notifyError($message, $code = 0)
269    {
270        $ctx = [
271            'success' => false,
272            'state' => $this->getTestSession()->getState(),
273            'message' => $message,
274            'code' => $code,
275        ];
276
277        $this->setData('assessmentTestContext', $ctx);
278
279        if (\tao_helpers_Request::isAjax()) {
280            $this->returnJson($ctx);
281        }
282    }
283
284    /**
285     * Common stuff processessed on almost all actions.
286     * If something goes wrong, print a report and return false, otherwise return true.
287     * @param bool $notifyError Allow to print error message if needed
288     * @return bool Returns a flag telling whether or not the action can be processed
289     * @throws \common_Exception
290     * @throws \InvalidArgumentException
291     * @throws common_exception_Error
292     * @throws common_exception_InconsistentData
293     * @throws common_ext_ExtensionException
294     */
295    protected function beforeAction($notifyError = true)
296    {
297        // Controller initialization.
298        $this->retrieveTestDefinition($this->getRequestParameter('QtiTestCompilation'));
299
300        /** @var DeliveryServerService $deliveryServerService */
301        $deliveryServerService = $this->getServiceManager()->get(DeliveryServerService::SERVICE_ID);
302        $resultStore = $deliveryServerService->getResultStoreWrapper($this->getRequestParameter('serviceCallId'));
303
304        // Initialize storage and test session.
305        $testResource = new core_kernel_classes_Resource($this->getRequestParameter('QtiTestDefinition'));
306
307        $sessionManager = new taoQtiTest_helpers_SessionManager($resultStore, $testResource);
308        $userUri = common_session_SessionManager::getSession()->getUserUri();
309        $seeker = new BinaryAssessmentTestSeeker($this->getTestDefinition());
310
311        $config = \common_ext_ExtensionsManager::singleton()
312            ->getExtensionById('taoQtiTest')
313            ->getConfig('testRunner');
314        $storageClassName = $config['test-session-storage'];
315        $this->setStorage(new $storageClassName($sessionManager, $seeker, $userUri));
316
317        $this->retrieveTestSession();
318
319        // @TODO: use some storage to get the potential reason of the state (close/suspended)
320        $session = $this->getTestSession();
321        $state = $session->getState();
322        if ($state == AssessmentTestSessionState::CLOSED) {
323            if ($notifyError) {
324                $this->notifyError(
325                    __('The assessment has been terminated. You cannot interact with it anymore.'),
326                    $state
327                );
328            }
329            return false;
330        }
331
332        // @TODO: maybe use an option to enable this behavior
333        if ($state == AssessmentTestSessionState::SUSPENDED) {
334            if ($notifyError) {
335                $this->notifyError(
336                    // phpcs:disable Generic.Files.LineLength
337                    __('The assessment has been suspended. To resume your assessment, please relaunch it and contact your proctor if required.'),
338                    // phpcs:enable Generic.Files.LineLength
339                    $state
340                );
341            }
342            return false;
343        }
344
345        $sessionStateService = $this->getServiceManager()->get('taoQtiTest/SessionStateService');
346        $sessionStateService->resumeSession($session);
347
348        $this->retrieveTestMeta();
349        $this->retrieveItemIndex();
350
351        // Prevent anything to be cached by the client.
352        taoQtiTest_helpers_TestRunnerUtils::noHttpClientCache();
353
354        $metaData = $this->getMetaDataHandler()->getData();
355        if (!empty($metaData)) {
356            $this->getMetaDataHandler()->save($metaData);
357        }
358
359        return true;
360    }
361
362    /**
363     * Get instance og session metadata handler
364     *
365     * @return TestSessionMetaData
366     */
367    protected function getMetaDataHandler()
368    {
369        if ($this->metaDataHandler === null) {
370            $this->metaDataHandler = new TestSessionMetaData($this->getTestSession());
371        }
372        return $this->metaDataHandler;
373    }
374
375    /**
376     * Does some complementary stuff to finish the action. Builds the test context object and binds it to the response.
377     * @param bool $withContext
378     * @throws \qtism\runtime\storage\common\StorageException
379     */
380    protected function afterAction($withContext = true)
381    {
382        $testSession = $this->getTestSession();
383        $sessionId = $testSession->getSessionId();
384
385        // Build assessment test context.
386        $ctx = taoQtiTest_helpers_TestRunnerUtils::buildAssessmentTestContext(
387            $this->getTestSession(),
388            $this->getTestMeta(),
389            $this->getItemIndex(),
390            $this->getRequestParameter('QtiTestDefinition'),
391            $this->getRequestParameter('QtiTestCompilation'),
392            $this->getRequestParameter('standalone'),
393            $this->getCompilationDirectory()
394        );
395
396        // add a flag to allow distinction with error responses
397        $ctx['success'] = true;
398
399        // Put the assessment test context in request data.
400        $this->setData('assessmentTestContext', $ctx);
401
402        if ($withContext === true) {
403            // Output only if requested by client-code.
404            echo json_encode($ctx);
405        }
406
407        common_Logger::t("Persisting QTI Assessment Test Session '${sessionId}'...");
408        $this->getStorage()->persist($testSession);
409        $this->getServiceManager()->get(StorageManager::SERVICE_ID)->persist();
410    }
411
412    /**
413     * Main action of the TestRunner module.
414     *
415     */
416    public function index()
417    {
418        $config = \common_ext_ExtensionsManager::singleton()
419            ->getExtensionById('taoQtiTest')
420            ->getConfig('testRunner');
421        $noError = $this->beforeAction();
422
423        // this part is only accessible if beforeAction did not return an error
424        if ($noError) {
425            $session = $this->getTestSession();
426
427            /** @var \oat\taoQtiTest\models\SessionStateService $sessionStateService */
428            $sessionStateService = $this->getServiceManager()->get('taoQtiTest/SessionStateService');
429            $resetTimerAfterResume = isset($config['reset-timer-after-resume']) && $config['reset-timer-after-resume'];
430            if ($resetTimerAfterResume) {
431                $sessionStateService->updateTimeReference($session);
432            }
433            $this->setData(
434                'client_session_state_service',
435                $sessionStateService->getClientImplementation($resetTimerAfterResume)
436            );
437
438            if ($session->getState() === AssessmentTestSessionState::INITIAL) {
439                // The test has just been instantiated.
440                $session->beginTestSession();
441                common_Logger::i("Assessment Test Session begun.");
442            }
443
444            if (taoQtiTest_helpers_TestRunnerUtils::isTimeout($session) === false) {
445                taoQtiTest_helpers_TestRunnerUtils::beginCandidateInteraction($session);
446            }
447        }
448
449        // loads the specific config
450        // this part must be processed no matter if beforeAction returned an error:
451        // the context object is provided through the view
452        $this->setData('review_screen', !empty($config['test-taker-review']));
453        $this->setData('review_region', $config['test-taker-review-region'] ?? '');
454
455        $this->setData('client_config_url', $this->getClientConfigUrl());
456        $this->setData('client_timeout', $this->getClientTimeout());
457        $this->setView('test_runner.tpl');
458
459        // this part is only accessible if beforeAction did not return an error
460        if ($noError) {
461            $this->afterAction(false);
462        }
463    }
464
465    /**
466     * Keep item activity time up to date
467     * @throws \oat\oatbox\service\ServiceNotFoundException
468     * @throws common_Exception
469     * @throws common_ext_ExtensionException
470     * @throws \qtism\runtime\storage\common\StorageException
471     */
472    public function keepItemTimed()
473    {
474        if ($this->beforeAction()) {
475            $config = \common_ext_ExtensionsManager::singleton()
476                ->getExtensionById('taoQtiTest')
477                ->getConfig('testRunner');
478
479            if (
480                isset($config['reset-timer-after-resume'])
481                && $config['reset-timer-after-resume']
482                && $this->hasRequestParameter('duration')
483            ) {
484                $session = $this->getTestSession();
485
486                // originally in milliseconds, but we have to convert to seconds now
487                $durationInSeconds = (int) ($this->getRequestParameter('duration') / 1000);
488
489                $time = new \DateTime('now', new \DateTimeZone('UTC'));
490                $duration = new DateInterval('PT' . $durationInSeconds . 'S');
491                $time->sub($duration);
492
493                /** @var \oat\taoQtiTest\models\SessionStateService $sessionStateService */
494                $sessionStateService = $this->getServiceManager()->get('taoQtiTest/SessionStateService');
495                $sessionStateService->updateTimeReference($session, $time);
496                $this->afterAction();
497            }
498        }
499    }
500
501    /**
502     * Mark an item for review in the Assessment Test Session flow.
503     *
504     */
505    public function markForReview()
506    {
507        if ($this->beforeAction()) {
508            $testSession = $this->getTestSession();
509            $sessionId = $testSession->getSessionId();
510
511            try {
512                if ($this->hasRequestParameter('position')) {
513                    $itemPosition = intval($this->getRequestParameter('position'));
514                } else {
515                    $itemPosition = $testSession->getRoute()->getPosition();
516                }
517                if ($this->hasRequestParameter('flag')) {
518                    $flag = $this->getRequestParameter('flag');
519                    if (is_numeric($flag)) {
520                        $flag = !!(intval($flag));
521                    } else {
522                        $flag = 'false' != strtolower($flag);
523                    }
524                } else {
525                    $flag = true;
526                }
527                taoQtiTest_helpers_TestRunnerUtils::setItemFlag($testSession, $itemPosition, $flag);
528
529                $this->returnJson([
530                    'success' => true,
531                    'position' => $itemPosition,
532                    'flag' => $flag
533                ]);
534            } catch (AssessmentTestSessionException $e) {
535                $this->handleAssessmentTestSessionException($e);
536            }
537
538            common_Logger::t("Persisting QTI Assessment Test Session '${sessionId}'...");
539            $this->getStorage()->persist($testSession);
540        }
541    }
542
543    /**
544     * Jump to an item in the Assessment Test Session flow.
545     *
546     */
547    public function jumpTo()
548    {
549        if ($this->beforeAction()) {
550            $session = $this->getTestSession();
551            $nextPosition = intval($this->getRequestParameter('position'));
552
553            try {
554                $this->endTimedSection($nextPosition);
555
556                $session->jumpTo($nextPosition);
557
558                if (
559                    $session->isRunning() === true
560                    && taoQtiTest_helpers_TestRunnerUtils::isTimeout($session) === false
561                ) {
562                    taoQtiTest_helpers_TestRunnerUtils::beginCandidateInteraction($session);
563                }
564            } catch (AssessmentTestSessionException $e) {
565                $this->handleAssessmentTestSessionException($e);
566            }
567
568            $this->afterAction();
569        }
570    }
571
572    protected function endTimedSection($nextPosition)
573    {
574        $config = \common_ext_ExtensionsManager::singleton()
575            ->getExtensionById('taoQtiTest')
576            ->getConfig('testRunner');
577
578        if (empty($config['keep-timer-up-to-timeout'])) {
579            $isJumpOutOfSection = false;
580            $session = $this->getTestSession();
581            $section = $session->getCurrentAssessmentSection();
582
583            $route = $session->getRoute();
584
585            if (($nextPosition >= 0) && ($nextPosition < $route->count())) {
586                $nextSection = $route->getRouteItemAt($nextPosition);
587
588                // phpcs:disable Generic.Files.LineLength
589                $isJumpOutOfSection = ($section->getIdentifier() !== $nextSection->getAssessmentSection()->getIdentifier());
590                // phpcs:enable Generic.Files.LineLength
591            }
592
593            $limits = $section->getTimeLimits();
594
595            //ensure that jumping out and section is timed
596            if ($isJumpOutOfSection && $limits != null && $limits->hasMaxTime()) {
597                $components = $section->getComponents();
598
599                foreach ($components as $object) {
600                    if ($object instanceof \qtism\data\ExtendedAssessmentItemRef) {
601                        $items = $session->getAssessmentItemSessions($object->getIdentifier());
602
603                        foreach ($items as $item) {
604                            if ($item instanceof \qtism\runtime\tests\AssessmentItemSession) {
605                                $item->endItemSession();
606                            }
607                        }
608                    }
609                }
610            }
611        }
612    }
613
614    /**
615     * Move forward in the Assessment Test Session flow.
616     *
617     */
618    public function moveForward()
619    {
620        if ($this->beforeAction()) {
621            $session = $this->getTestSession();
622            $nextPosition = $session->getRoute()->getPosition() + 1;
623
624            try {
625                $this->endTimedSection($nextPosition);
626
627                $session->moveNext();
628
629                if (
630                    $session->isRunning() === true
631                    && taoQtiTest_helpers_TestRunnerUtils::isTimeout($session) === false
632                ) {
633                    taoQtiTest_helpers_TestRunnerUtils::beginCandidateInteraction($session);
634                }
635            } catch (AssessmentTestSessionException $e) {
636                $this->handleAssessmentTestSessionException($e);
637            }
638
639            $this->afterAction();
640        }
641    }
642
643    /**
644     * Move backward in the Assessment Test Session flow.
645     *
646     */
647    public function moveBackward()
648    {
649        if ($this->beforeAction()) {
650            $session = $this->getTestSession();
651            $nextPosition = $session->getRoute()->getPosition() - 1;
652
653            try {
654                $this->endTimedSection($nextPosition);
655
656                $session->moveBack();
657
658                if (taoQtiTest_helpers_TestRunnerUtils::isTimeout($session) === false) {
659                    taoQtiTest_helpers_TestRunnerUtils::beginCandidateInteraction($session);
660                }
661            } catch (AssessmentTestSessionException $e) {
662                $this->handleAssessmentTestSessionException($e);
663            }
664
665            $this->afterAction();
666        }
667    }
668
669    /**
670     * Moves to the next available section in the Assessment Test Session flow.
671     *
672     */
673    public function nextSection()
674    {
675        if ($this->beforeAction()) {
676            $session = $this->getTestSession();
677
678            try {
679                $session->moveNextAssessmentSection();
680
681                if (
682                    $session->isRunning() === true
683                    && taoQtiTest_helpers_TestRunnerUtils::isTimeout($session) === false
684                ) {
685                    taoQtiTest_helpers_TestRunnerUtils::beginCandidateInteraction($session);
686                }
687            } catch (AssessmentTestSessionException $e) {
688                $this->handleAssessmentTestSessionException($e);
689            }
690
691            $this->afterAction();
692        }
693    }
694
695    /**
696     * Skip the current item in the Assessment Test Session flow.
697     *
698     */
699    public function skip()
700    {
701        if ($this->beforeAction()) {
702            $session = $this->getTestSession();
703
704            try {
705                $session->skip();
706                $session->moveNext();
707
708                if (
709                    $session->isRunning() === true
710                    && taoQtiTest_helpers_TestRunnerUtils::isTimeout($session) === false
711                ) {
712                    taoQtiTest_helpers_TestRunnerUtils::beginCandidateInteraction($session);
713                }
714            } catch (AssessmentTestSessionException $e) {
715                $this->handleAssessmentTestSessionException($e);
716            }
717
718            $this->afterAction();
719        }
720    }
721
722    /**
723     * Action to call when a structural QTI component times out in linear mode.
724     *
725     */
726    public function timeout()
727    {
728        if ($this->beforeAction()) {
729            $session = $this->getTestSession();
730
731            try {
732                $session->checkTimeLimits(false, true, false);
733            } catch (AssessmentTestSessionException $e) {
734                $this->onTimeout($e);
735            }
736
737            // If we are here, without executing onTimeout() there is an inconsistency. Simply respond
738            // to the client with the actual assessment test context. Maybe the client will be able to
739            // continue...
740            $this->afterAction();
741        }
742    }
743
744    /**
745     * Action to end test session
746     */
747    public function endTestSession()
748    {
749        if ($this->beforeAction()) {
750            $session = $this->getTestSession();
751            $sessionId = $session->getSessionId();
752
753            common_Logger::i("The user has requested termination of the test session '{$sessionId}'");
754            $session->endTestSession();
755
756            $this->afterAction();
757        }
758    }
759
760    /**
761     * Stuff to be undertaken when the Assessment Item presented to the candidate
762     * times out.
763     *
764     * @param AssessmentTestSessionException $timeOutException The AssessmentTestSessionException object thrown to
765     *                                                         indicate the timeout.
766     */
767    protected function onTimeout(AssessmentTestSessionException $timeOutException)
768    {
769        $session = $this->getTestSession();
770
771        if ($session->getCurrentNavigationMode() === NavigationMode::LINEAR) {
772            switch ($timeOutException->getCode()) {
773                case AssessmentTestSessionException::ASSESSMENT_TEST_DURATION_OVERFLOW:
774                    $session->endTestSession();
775                    break;
776
777                case AssessmentTestSessionException::TEST_PART_DURATION_OVERFLOW:
778                    $session->moveNextTestPart();
779                    break;
780
781                case AssessmentTestSessionException::ASSESSMENT_SECTION_DURATION_OVERFLOW:
782                    $session->moveNextAssessmentSection();
783                    break;
784
785                case AssessmentTestSessionException::ASSESSMENT_ITEM_DURATION_OVERFLOW:
786                    $session->moveNextAssessmentItem();
787                    break;
788            }
789
790            if ($session->isRunning() === true && taoQtiTest_helpers_TestRunnerUtils::isTimeout($session) === false) {
791                taoQtiTest_helpers_TestRunnerUtils::beginCandidateInteraction($session);
792            }
793        } else {
794            $itemSession = $session->getCurrentAssessmentItemSession();
795            $itemSession->endItemSession();
796        }
797    }
798
799    /**
800     * Action called when a QTI Item embedded in a QTI Test submit responses.
801     *
802     */
803    public function storeItemVariableSet()
804    {
805        if ($this->beforeAction()) {
806            // --- Deal with provided responses.
807            $jsonPayload = taoQtiCommon_helpers_Utils::readJsonPayload();
808
809            $responses = new State();
810            $currentItem = $this->getTestSession()->getCurrentAssessmentItemRef();
811            $currentOccurence = $this->getTestSession()->getCurrentAssessmentItemRefOccurence();
812
813            if ($currentItem === false) {
814                $msg = "Trying to store item variables but the state of the test session is INITIAL or CLOSED.\n";
815                $msg .= "Session state value: " . $this->getTestSession()->getState() . "\n";
816                $msg .= "Session ID: " . $this->getTestSession()->getSessionId() . "\n";
817                $msg .= "JSON Payload: " . mb_substr(json_encode($jsonPayload), 0, 1000);
818                common_Logger::e($msg);
819            }
820
821            $filler = new taoQtiCommon_helpers_PciVariableFiller(
822                $currentItem,
823                ServiceManager::getServiceManager()->get(QtiFlysystemFileManager::SERVICE_ID)
824            );
825
826            if (is_array($jsonPayload)) {
827                foreach ($jsonPayload as $id => $response) {
828                    try {
829                        $var = $filler->fill($id, $response);
830                        // Do not take into account QTI File placeholders.
831                        if (taoQtiCommon_helpers_Utils::isQtiFilePlaceHolder($var) === false) {
832                            $responses->setVariable($var);
833                        }
834                    } catch (OutOfRangeException $e) {
835                        common_Logger::d("Could not convert client-side value for variable '${id}'.");
836                    } catch (OutOfBoundsException $e) {
837                        common_Logger::d("Could not find variable with identifier '${id}' in current item.");
838                    }
839                }
840            } else {
841                common_Logger::e('Invalid json payload');
842            }
843
844            $displayFeedback = $this->getTestSession()->getCurrentSubmissionMode() !== SubmissionMode::SIMULTANEOUS;
845            $stateOutput = new taoQtiCommon_helpers_PciStateOutput();
846
847            try {
848                common_Logger::t('Responses sent from the client-side. The Response Processing will take place.');
849                $this->getTestSession()->endAttempt($responses, true);
850
851                // Return the item session state to the client side.
852                $itemSession = $this->getTestSession()->getAssessmentItemSessionStore()->getAssessmentItemSession(
853                    $currentItem,
854                    $currentOccurence
855                );
856
857                foreach ($itemSession->getAllVariables() as $var) {
858                    $stateOutput->addVariable($var);
859                }
860
861                $itemCompilationDirectory = $this->getDirectory($this->getRequestParameter('itemDataPath'));
862                $jsonReturn = ['success' => true,
863                    'displayFeedback' => $displayFeedback,
864                    'itemSession' => $stateOutput->getOutput(),
865                    'feedbacks' => []];
866
867                if ($displayFeedback === true) {
868                    $jsonReturn['feedbacks'] = QtiRunner::getFeedbacks($itemCompilationDirectory, $itemSession);
869                }
870
871                echo json_encode($jsonReturn);
872            } catch (AssessmentTestSessionException $e) {
873                $this->handleAssessmentTestSessionException($e);
874            }
875
876            $this->afterAction(false);
877        }
878    }
879
880    /**
881     * Action to call to comment an item.
882     *
883     */
884    public function comment()
885    {
886        if ($this->beforeAction()) {
887            $testSession = $this->getTestSession();
888
889            // prepare transmission Id for result server.
890            $item = $testSession->getCurrentAssessmentItemRef()->getIdentifier();
891            $occurence = $testSession->getCurrentAssessmentItemRefOccurence();
892            $sessionId = $testSession->getSessionId();
893            $transmissionId = "${sessionId}.${item}.${occurence}";
894
895            // retrieve comment's intrinsic value.
896            $comment = $this->getRequestParameter('comment');
897
898            /** @var DeliveryServerService $deliveryServerService */
899            $deliveryServerService = $this->getServiceManager()->get(DeliveryServerService::SERVICE_ID);
900            $resultStore = $deliveryServerService->getResultStoreWrapper($sessionId);
901
902            // build variable and send it.
903            $itemUri = taoQtiTest_helpers_TestRunnerUtils::getCurrentItemUri($testSession);
904            $testUri = $testSession->getTest()->getUri();
905            $variable = new ResponseVariable(
906                'comment',
907                Cardinality::SINGLE,
908                BaseType::STRING,
909                new QtismString($comment)
910            );
911
912            $transmitter = new taoQtiCommon_helpers_ResultTransmitter($resultStore);
913            $transmitter->transmitItemVariable($variable, $transmissionId, $itemUri, $testUri);
914        }
915    }
916
917    /**
918     * Retrieve the Test Definition the test session is built
919     * from as an AssessmentTest object. This method
920     * also retrieves the compilation directory.
921     *
922     * @param string $qtiTestCompilation (e.g. <i>'http://sample/first.rdf#i14363448108243883-
923     *                                   |http://sample/first.rdf#i14363448109065884+'</i>)
924     *
925     * @return AssessmentTest The AssessmentTest object the current test session is built from.
926     */
927    protected function retrieveTestDefinition($qtiTestCompilation)
928    {
929        $directoryIds = explode('|', $qtiTestCompilation);
930        $directories = [
931            'private' => $this->getDirectory($directoryIds[0]),
932            'public' => $this->getDirectory($directoryIds[1])
933        ];
934
935        $this->setCompilationDirectory($directories);
936        $testDefinition = \taoQtiTest_helpers_Utils::getTestDefinition($qtiTestCompilation);
937        $this->setTestDefinition($testDefinition);
938    }
939
940    /**
941     * Retrieve the current test session as an AssessmentTestSession object from
942     * persistent storage.
943     *
944     */
945    protected function retrieveTestSession()
946    {
947        $qtiStorage = $this->getStorage();
948        $sessionId = $this->getServiceCallId();
949
950        if ($qtiStorage->exists($sessionId) === false) {
951            common_Logger::t("Instantiating QTI Assessment Test Session");
952            $this->setTestSession($qtiStorage->instantiate($this->getTestDefinition(), $sessionId));
953
954            $testTaker = \common_session_SessionManager::getSession()->getUser();
955            taoQtiTest_helpers_TestRunnerUtils::setInitialOutcomes($this->getTestSession(), $testTaker);
956        } else {
957            common_Logger::t("Retrieving QTI Assessment Test Session '${sessionId}'...");
958            $this->setTestSession($qtiStorage->retrieve($this->getTestDefinition(), $sessionId));
959        }
960
961        taoQtiTest_helpers_TestRunnerUtils::preserveOutcomes($this->getTestSession());
962    }
963
964    /**
965     * Retrieve the QTI Test Definition meta-data array stored
966     * into the private compilation directory.
967     *
968     * @return array
969     * @throws common_exception_InconsistentData
970     */
971    protected function retrieveTestMeta()
972    {
973        $directories = $this->getCompilationDirectory();
974        /** @var tao_models_classes_service_StorageDirectory $privateDirectory */
975        $privateDirectory = $directories['private'];
976
977        /** @var CompilationDataService $compilationDataService */
978        $compilationDataService = $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID);
979        $this->setTestMeta($compilationDataService->readCompilationMetadata($privateDirectory));
980    }
981
982    /**
983     * Retrieves the index of compiled items.
984     */
985    protected function retrieveItemIndex()
986    {
987        $this->setItemIndex(new QtiTestCompilerIndex());
988        try {
989            $directories = $this->getCompilationDirectory();
990            /** @var tao_models_classes_service_StorageDirectory $privateDirectory */
991            $privateDirectory = $directories['private'];
992            $data = $privateDirectory->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_INDEX);
993            if ($data) {
994                $this->getItemIndex()->unserialize($data);
995            }
996        } catch (\Exception $e) {
997            \common_Logger::d('Ignoring file not found exception for Items Index');
998        }
999    }
1000
1001    protected function handleAssessmentTestSessionException(AssessmentTestSessionException $e)
1002    {
1003        switch ($e->getCode()) {
1004            case AssessmentTestSessionException::ASSESSMENT_TEST_DURATION_OVERFLOW:
1005            case AssessmentTestSessionException::TEST_PART_DURATION_OVERFLOW:
1006            case AssessmentTestSessionException::ASSESSMENT_SECTION_DURATION_OVERFLOW:
1007            case AssessmentTestSessionException::ASSESSMENT_ITEM_DURATION_OVERFLOW:
1008                $this->onTimeout($e);
1009                break;
1010
1011            default:
1012                $msg = "Non managed QTI Test exception caught:\n";
1013
1014                do {
1015                    $msg .= "[" . get_class($e) . "] " . $e->getMessage() . "\n";
1016                } while ($e = $e->getPrevious());
1017
1018                common_Logger::e($msg);
1019                break;
1020        }
1021    }
1022}