1<?php
2/* Copyright (c) 1998-2013 ILIAS open source, Extended GPL, see docs/LICENSE */
3
4require_once './Modules/Test/classes/inc.AssessmentConstants.php';
5require_once './Modules/Test/classes/class.ilTestPlayerCommands.php';
6require_once './Modules/Test/classes/class.ilTestServiceGUI.php';
7require_once './Modules/TestQuestionPool/classes/class.assQuestion.php';
8require_once './Services/UIComponent/Button/classes/class.ilSubmitButton.php';
9require_once 'Modules/Test/classes/class.ilTestPlayerNavButton.php';
10
11/**
12 * Output class for assessment test execution
13 *
14 * The ilTestOutputGUI class creates the output for the ilObjTestGUI class when learners execute a test. This saves
15 * some heap space because the ilObjTestGUI class will be much smaller then
16 *
17 * @author		Björn Heyser <bheyser@databay.de>
18 * @author		Maximilian Becker <mbecker@databay.de>
19 *
20 * @version		$Id$
21 *
22 * @inGroup		ModulesTest
23 *
24 */
25abstract class ilTestPlayerAbstractGUI extends ilTestServiceGUI
26{
27    const PRESENTATION_MODE_VIEW = 'view';
28    const PRESENTATION_MODE_EDIT = 'edit';
29
30    const FIXED_SHUFFLER_SEED_MIN_LENGTH = 8;
31
32    public $ref_id;
33    public $saveResult;
34    public $sequence;
35    public $cmdCtrl;
36    public $maxProcessingTimeReached;
37    public $endingTimeReached;
38
39    /**
40     * @var ilTestPasswordChecker
41     */
42    protected $passwordChecker;
43
44    /**
45     * @var ilTestProcessLocker
46     */
47    protected $processLocker;
48
49    /**
50     * @var ilTestSession
51     */
52    protected $testSession;
53
54    /**
55     * @var ilSetting
56     */
57    protected $assSettings;
58
59    /**
60     * @var ilTestSequence|ilTestSequenceDynamicQuestionSet
61     */
62    protected $testSequence = null;
63
64    /**
65    * ilTestOutputGUI constructor
66    *
67    * @param ilObjTest $a_object
68    */
69    public function __construct($a_object)
70    {
71        parent::__construct($a_object);
72        $this->ref_id = $_GET["ref_id"];
73
74        global $DIC;
75        $rbacsystem = $DIC['rbacsystem'];
76        $ilUser = $DIC['ilUser'];
77        $lng = $DIC['lng'];
78        require_once 'Modules/Test/classes/class.ilTestPasswordChecker.php';
79        $this->passwordChecker = new ilTestPasswordChecker($rbacsystem, $ilUser, $this->object, $lng);
80
81        $this->processLocker = null;
82        $this->testSession = null;
83        $this->assSettings = null;
84    }
85
86    protected function checkReadAccess()
87    {
88        global $DIC;
89        $rbacsystem = $DIC['rbacsystem'];
90
91        if (!$rbacsystem->checkAccess("read", $this->object->getRefId())) {
92            // only with read access it is possible to run the test
93            $this->ilias->raiseError($this->lng->txt("cannot_execute_test"), $this->ilias->error_obj->MESSAGE);
94        }
95    }
96
97    protected function checkTestExecutable()
98    {
99        $executable = $this->object->isExecutable($this->testSession, $this->testSession->getUserId());
100
101        if (!$executable['executable']) {
102            ilUtil::sendInfo($executable['errormessage'], true);
103            $this->ctrl->redirectByClass("ilobjtestgui", "infoScreen");
104        }
105    }
106
107    protected function checkTestSessionUser(ilTestSession $testSession)
108    {
109        global $DIC; /* @var ILIAS\DI\Container $DIC */
110
111        if ($testSession->getUserId() != $DIC->user()->getId()) {
112            throw new ilTestException('active id given does not relate to current user!');
113        }
114    }
115
116    protected function ensureExistingTestSession(ilTestSession $testSession)
117    {
118        if ($testSession->getActiveId()) {
119            return;
120        }
121
122        global $DIC;
123        $ilUser = $DIC['ilUser'];
124
125        $testSession->setUserId($ilUser->getId());
126
127        if ($testSession->isAnonymousUser()) {
128            if (!$testSession->doesAccessCodeInSessionExists()) {
129                return;
130            }
131
132            $testSession->setAnonymousId($testSession->getAccessCodeFromSession());
133        }
134
135        $testSession->saveToDb();
136    }
137
138    protected function initProcessLocker($activeId)
139    {
140        global $DIC;
141        $ilDB = $DIC['ilDB'];
142
143        require_once 'Modules/Test/classes/class.ilTestProcessLockerFactory.php';
144        $processLockerFactory = new ilTestProcessLockerFactory($this->assSettings, $ilDB);
145
146        $processLockerFactory->setActiveId($activeId);
147
148        $this->processLocker = $processLockerFactory->getLocker();
149    }
150
151    /**
152     * Save tags for tagging gui
153     *
154     * Needed this function here because the test info page
155     * uses another class to send its form results
156     */
157    public function saveTagsCmd()
158    {
159        include_once("./Services/Tagging/classes/class.ilTaggingGUI.php");
160        $tagging_gui = new ilTaggingGUI();
161        $tagging_gui->setObject($this->object->getId(), $this->object->getType());
162        $tagging_gui->saveInput();
163        $this->ctrl->redirectByClass("ilobjtestgui", "infoScreen");
164    }
165
166    /**
167     * updates working time and stores state saveresult to see if question has to be stored or not
168     */
169    public function updateWorkingTime()
170    {
171        if ($_SESSION["active_time_id"]) {
172            $this->object->updateWorkingTime($_SESSION["active_time_id"]);
173        }
174
175        $_SESSION["active_time_id"] = $this->object->startWorkingTime(
176            $this->testSession->getActiveId(),
177            $this->testSession->getPass()
178        );
179    }
180
181    // fau: testNav - new function removeIntermediateSolution()
182    /**
183     * remove an auto-saved solution of the current question
184     * @return mixed	number of rows or db error
185     */
186    public function removeIntermediateSolution()
187    {
188        $questionId = $this->getCurrentQuestionId();
189
190        $this->getQuestionInstance($questionId)->removeIntermediateSolution(
191            $this->testSession->getActiveId(),
192            $this->testSession->getPass()
193        );
194    }
195    // fau.
196
197    /**
198     * saves the user input of a question
199     */
200    abstract public function saveQuestionSolution($authorized = true, $force = false);
201
202    abstract protected function canSaveResult();
203
204    public function suspendTestCmd()
205    {
206        $this->ctrl->redirectByClass("ilobjtestgui", "infoScreen");
207    }
208
209    /**
210    * Checks wheather the maximum processing time is reached or not
211    *
212    * Checks wheather the maximum processing time is reached or not
213    *
214    * @return bool TRUE if the maximum processing time is reached, FALSE otherwise
215    */
216    public function isMaxProcessingTimeReached()
217    {
218        global $DIC;
219        $ilUser = $DIC['ilUser'];
220        $active_id = $this->testSession->getActiveId();
221        $starting_time = $this->object->getStartingTimeOfUser($active_id);
222        if ($starting_time === false) {
223            return false;
224        } else {
225            return $this->object->isMaxProcessingTimeReached($starting_time, $active_id);
226        }
227    }
228
229    protected function determineInlineScoreDisplay()
230    {
231        $show_question_inline_score = false;
232        if ($this->object->getAnswerFeedbackPoints()) {
233            $show_question_inline_score = true;
234            return $show_question_inline_score;
235        }
236        return $show_question_inline_score;
237    }
238
239    protected function populateTestNavigationToolbar(ilTestNavigationToolbarGUI $toolbarGUI)
240    {
241        $this->tpl->setCurrentBlock('test_nav_toolbar');
242        $this->tpl->setVariable('TEST_NAV_TOOLBAR', $toolbarGUI->getHTML());
243        $this->tpl->parseCurrentBlock();
244    }
245
246    protected function populateQuestionNavigation($sequenceElement, $disabled, $primaryNext)
247    {
248        if (!$this->isFirstQuestionInSequence($sequenceElement)) {
249            $this->populatePreviousButtons($disabled);
250        }
251
252        if (!$this->isLastQuestionInSequence($sequenceElement)) {
253            $this->populateNextButtons($disabled, $primaryNext);
254        }
255    }
256
257    protected function populatePreviousButtons($disabled)
258    {
259        $this->populateUpperPreviousButtonBlock($disabled);
260        $this->populateLowerPreviousButtonBlock($disabled);
261    }
262
263    protected function populateNextButtons($disabled, $primaryNext)
264    {
265        $this->populateUpperNextButtonBlock($disabled, $primaryNext);
266        $this->populateLowerNextButtonBlock($disabled, $primaryNext);
267    }
268
269    protected function populateLowerNextButtonBlock($disabled, $primaryNext)
270    {
271        $button = $this->buildNextButtonInstance($disabled, $primaryNext);
272        $button->setId('bottomnextbutton');
273
274        $this->tpl->setCurrentBlock("next_bottom");
275        $this->tpl->setVariable("BTN_NEXT", $button->render());
276        $this->tpl->parseCurrentBlock();
277    }
278
279    protected function populateUpperNextButtonBlock($disabled, $primaryNext)
280    {
281        $button = $this->buildNextButtonInstance($disabled, $primaryNext);
282        $button->setId('nextbutton');
283
284        $this->tpl->setCurrentBlock("next");
285        $this->tpl->setVariable("BTN_NEXT", $button->render());
286        $this->tpl->parseCurrentBlock();
287    }
288
289    protected function populateLowerPreviousButtonBlock($disabled)
290    {
291        $button = $this->buildPreviousButtonInstance($disabled);
292        $button->setId('bottomprevbutton');
293
294        $this->tpl->setCurrentBlock("prev_bottom");
295        $this->tpl->setVariable("BTN_PREV", $button->render());
296        $this->tpl->parseCurrentBlock();
297    }
298
299    protected function populateUpperPreviousButtonBlock($disabled)
300    {
301        $button = $this->buildPreviousButtonInstance($disabled);
302        $button->setId('prevbutton');
303
304        $this->tpl->setCurrentBlock("prev");
305        $this->tpl->setVariable("BTN_PREV", $button->render());
306        $this->tpl->parseCurrentBlock();
307    }
308
309    /**
310     * @param bool $disabled
311     * @param bool $primaryNext
312     * @return ilButtonBase|ilLinkButton|ilTestPlayerNavButton
313     */
314    private function buildNextButtonInstance($disabled, $primaryNext)
315    {
316        $button = ilTestPlayerNavButton::getInstance();
317        // fau: testNav - set glyphicon and primary
318        $button->setPrimary($primaryNext);
319        $button->setRightGlyph('glyphicon glyphicon-arrow-right');
320        // fau.
321        $button->setNextCommand(ilTestPlayerCommands::NEXT_QUESTION);
322        $button->setUrl($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::NEXT_QUESTION));
323        $button->setCaption('next_question');
324        $button->addCSSClass('ilTstNavElem');
325        //$button->setDisabled($disabled);
326        return $button;
327    }
328
329    /**
330     * @param $disabled
331     * @return ilTestPlayerNavButton
332     */
333    private function buildPreviousButtonInstance($disabled)
334    {
335        $button = ilTestPlayerNavButton::getInstance();
336        // fau: testNav - set glyphicon and primary
337        $button->setLeftGlyph('glyphicon glyphicon-arrow-left');
338        // fau.
339        $button->setNextCommand(ilTestPlayerCommands::PREVIOUS_QUESTION);
340        $button->setUrl($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::PREVIOUS_QUESTION));
341        $button->setCaption('previous_question');
342        $button->addCSSClass('ilTstNavElem');
343        //$button->setDisabled($disabled);
344        return $button;
345    }
346
347    protected function populateSpecificFeedbackBlock(assQuestionGUI $question_gui)
348    {
349        $solutionValues = $question_gui->object->getSolutionValues(
350            $this->testSession->getActiveId(),
351            null
352        );
353
354        $feedback = $question_gui->getSpecificFeedbackOutput(
355            $question_gui->object->fetchIndexedValuesFromValuePairs($solutionValues)
356        );
357
358        $this->tpl->setCurrentBlock("specific_feedback");
359        $this->tpl->setVariable("SPECIFIC_FEEDBACK", $feedback);
360        $this->tpl->parseCurrentBlock();
361    }
362
363    protected function populateGenericFeedbackBlock(assQuestionGUI $question_gui, $solutionCorrect)
364    {
365        $feedback = $question_gui->getGenericFeedbackOutput($this->testSession->getActiveId(), null);
366
367        if (strlen($feedback)) {
368            $cssClass = (
369                $solutionCorrect ?
370                ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_CORRECT : ilAssQuestionFeedback::CSS_CLASS_FEEDBACK_WRONG
371            );
372
373            $this->tpl->setCurrentBlock("answer_feedback");
374            $this->tpl->setVariable("ANSWER_FEEDBACK", $feedback);
375            $this->tpl->setVariable("ILC_FB_CSS_CLASS", $cssClass);
376            $this->tpl->parseCurrentBlock();
377        }
378    }
379
380    protected function populateScoreBlock($reachedPoints, $maxPoints)
381    {
382        $scoreInformation = sprintf(
383            $this->lng->txt("you_received_a_of_b_points"),
384            $reachedPoints,
385            $maxPoints
386        );
387
388        $this->tpl->setCurrentBlock("received_points_information");
389        $this->tpl->setVariable("RECEIVED_POINTS_INFORMATION", $scoreInformation);
390        $this->tpl->parseCurrentBlock();
391    }
392
393    protected function populateSolutionBlock($solutionoutput)
394    {
395        if (strlen($solutionoutput)) {
396            $this->tpl->setCurrentBlock("solution_output");
397            $this->tpl->setVariable("CORRECT_SOLUTION", $this->lng->txt("tst_best_solution_is"));
398            $this->tpl->setVariable("QUESTION_FEEDBACK", $solutionoutput);
399            $this->tpl->parseCurrentBlock();
400        }
401    }
402
403    protected function populateSyntaxStyleBlock()
404    {
405        $this->tpl->setCurrentBlock("SyntaxStyle");
406        $this->tpl->setVariable(
407            "LOCATION_SYNTAX_STYLESHEET",
408            ilObjStyleSheet::getSyntaxStylePath()
409        );
410        $this->tpl->parseCurrentBlock();
411    }
412
413    protected function populateContentStyleBlock()
414    {
415        include_once("./Services/Style/Content/classes/class.ilObjStyleSheet.php");
416        $this->tpl->setCurrentBlock("ContentStyle");
417        $this->tpl->setVariable(
418            "LOCATION_CONTENT_STYLESHEET",
419            ilObjStyleSheet::getContentStylePath(0)
420        );
421        $this->tpl->parseCurrentBlock();
422    }
423
424    /**
425     * Sets a session variable with the test access code for an anonymous test user
426     *
427     * Sets a session variable with the test access code for an anonymous test user
428     */
429    public function setAnonymousIdCmd()
430    {
431        if ($this->testSession->isAnonymousUser()) {
432            $this->testSession->setAccessCodeToSession($_POST['anonymous_id']);
433        }
434
435        $this->ctrl->redirectByClass("ilobjtestgui", "infoScreen");
436    }
437
438    /**
439     * Start a test for the first time
440     *
441     * Start a test for the first time. This method contains a lock
442     * to prevent multiple submissions by the start test button
443     */
444    protected function startPlayerCmd()
445    {
446        $testStartLock = $this->getLockParameter();
447        $isFirstTestStartRequest = false;
448
449        $this->processLocker->executeTestStartLockOperation(function () use ($testStartLock, &$isFirstTestStartRequest) {
450            if ($this->testSession->lookupTestStartLock() != $testStartLock) {
451                $this->testSession->persistTestStartLock($testStartLock);
452                $isFirstTestStartRequest = true;
453            }
454        });
455
456        if ($isFirstTestStartRequest) {
457            $this->handleUserSettings();
458            $this->ctrl->redirect($this, ilTestPlayerCommands::INIT_TEST);
459        }
460
461        $this->ctrl->setParameterByClass('ilObjTestGUI', 'lock', $testStartLock);
462        $this->ctrl->redirectByClass("ilobjtestgui", "redirectToInfoScreen");
463    }
464
465    public function getLockParameter()
466    {
467        if (isset($_POST['lock']) && strlen($_POST['lock'])) {
468            return $_POST['lock'];
469        } elseif (isset($_GET['lock']) && strlen($_GET['lock'])) {
470            return $_GET['lock'];
471        }
472
473        return null;
474    }
475
476    /**
477     * Resume a test at the last position
478     */
479    abstract protected function resumePlayerCmd();
480
481    /**
482     * Start a test for the first time after a redirect
483     */
484    protected function initTestCmd()
485    {
486        if ($this->object->checkMaximumAllowedUsers() == false) {
487            return $this->showMaximumAllowedUsersReachedMessage();
488        }
489
490        if ($this->testSession->isAnonymousUser() && !$this->testSession->getActiveId()) {
491            $accessCode = $this->testSession->createNewAccessCode();
492
493            $this->testSession->setAccessCodeToSession($accessCode);
494            $this->testSession->setAnonymousId($accessCode);
495            $this->testSession->saveToDb();
496
497            $this->ctrl->redirect($this, ilTestPlayerCommands::DISPLAY_ACCESS_CODE);
498        }
499
500        $this->testSession->unsetAccessCodeInSession();
501        $this->ctrl->redirect($this, ilTestPlayerCommands::START_TEST);
502    }
503
504    public function displayAccessCodeCmd()
505    {
506        $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_anonymous_code_presentation.html", "Modules/Test");
507        $this->tpl->setCurrentBlock("adm_content");
508        $this->tpl->setVariable("TEXT_ANONYMOUS_CODE_CREATED", $this->lng->txt("tst_access_code_created"));
509        $this->tpl->setVariable("TEXT_ANONYMOUS_CODE", $this->testSession->getAccessCodeFromSession());
510        $this->tpl->setVariable("FORMACTION", $this->ctrl->getFormAction($this));
511        $this->tpl->setVariable("CMD_CONFIRM", ilTestPlayerCommands::ACCESS_CODE_CONFIRMED);
512        $this->tpl->setVariable("TXT_CONFIRM", $this->lng->txt("continue_work"));
513        $this->tpl->parseCurrentBlock();
514    }
515
516    public function accessCodeConfirmedCmd()
517    {
518        $this->ctrl->redirect($this, ilTestPlayerCommands::START_TEST);
519    }
520
521    /**
522     * Handles some form parameters on starting and resuming a test
523     */
524    public function handleUserSettings()
525    {
526        global $DIC;
527        $ilUser = $DIC['ilUser'];
528
529        if ($_POST["chb_javascript"]) {
530            $ilUser->writePref("tst_javascript", 1);
531        } else {
532            $ilUser->writePref("tst_javascript", 0);
533        }
534
535        // hide previous results
536        if ($this->object->getNrOfTries() != 1) {
537            if ($this->object->getUsePreviousAnswers() == 1) {
538                if ($_POST["chb_use_previous_answers"]) {
539                    $ilUser->writePref("tst_use_previous_answers", 1);
540                } else {
541                    $ilUser->writePref("tst_use_previous_answers", 0);
542                }
543            }
544        }
545    }
546
547    /**
548     * Redirect the user after an automatic save when the time limit is reached
549     * @throws ilTestException
550     */
551    public function redirectAfterAutosaveCmd()
552    {
553        $active_id = $this->testSession->getActiveId();
554        $actualpass = ilObjTest::_getPass($active_id);
555
556        $this->performTestPassFinishedTasks($actualpass);
557
558        $this->testSession->setLastFinishedPass($this->testSession->getPass());
559        $this->testSession->increaseTestPass();
560
561        $url = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::AFTER_TEST_PASS_FINISHED, '', false, false);
562
563        $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_redirect_autosave.html", "Modules/Test");
564        $this->tpl->setVariable("TEXT_REDIRECT", $this->lng->txt("redirectAfterSave"));
565        $this->tpl->setVariable("URL", $url);
566    }
567
568    public function redirectAfterDashboardCmd()
569    {
570        $active_id = $this->testSession->getActiveId();
571        $actualpass = ilObjTest::_getPass($active_id);
572
573        $this->performTestPassFinishedTasks($actualpass);
574
575        $this->testSession->setLastFinishedPass($this->testSession->getPass());
576        $this->testSession->increaseTestPass();
577
578        $url = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::AFTER_TEST_PASS_FINISHED, '', false, false);
579
580        $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_redirect_autosave.html", "Modules/Test");
581        $this->tpl->setVariable("TEXT_REDIRECT", $this->lng->txt("redirectAfterSave"));
582        $this->tpl->setVariable("URL", $url);
583    }
584
585    abstract protected function getCurrentQuestionId();
586
587    /**
588     * Automatically save a user answer while working on the test
589     * (called repeatedly by asynchronous posts in configured autosave interval)
590     */
591    public function autosaveCmd()
592    {
593        $result = "";
594        if (is_array($_POST) && count($_POST) > 0) {
595            if (!$this->canSaveResult() || $this->isParticipantsAnswerFixed($this->getCurrentQuestionId())) {
596                $result = '-IGNORE-';
597            } else {
598                // answer is changed from authorized solution, so save the change as intermediate solution
599                if ($this->getAnswerChangedParameter()) {
600                    $res = $this->saveQuestionSolution(false, true);
601                }
602                // answer is not changed from authorized solution, so delete an intermediate solution
603                else {
604                    $db_res = $this->removeIntermediateSolution();
605                    $res = is_int($db_res);
606                }
607                if ($res) {
608                    $result = $this->lng->txt("autosave_success");
609                } else {
610                    $result = $this->lng->txt("autosave_failed");
611                }
612            }
613        }
614        echo $result;
615        exit;
616    }
617
618    /**
619     * Automatically save a user answer when the limited duration of a test run is reached
620     * (called by synchronous form submit when the remaining time count down reaches zero)
621     */
622    public function autosaveOnTimeLimitCmd()
623    {
624        if (!$this->isParticipantsAnswerFixed($this->getCurrentQuestionId())) {
625            // time limit saves the user solution as authorized
626            $this->saveQuestionSolution(true, true);
627        }
628        $this->ctrl->redirect($this, ilTestPlayerCommands::REDIRECT_ON_TIME_LIMIT);
629    }
630
631
632    // fau: testNav - new function detectChangesCmd()
633    /**
634     * Detect changes sent in the background to the server
635     * This is called by ajax from ilTestPlayerQuestionEditControl.js
636     * It is needed by Java and Flash question and eventually plgin question vtypes
637     */
638    protected function detectChangesCmd()
639    {
640        $questionId = $this->getCurrentQuestionId();
641        $state = $this->getQuestionInstance($questionId)->lookupForExistingSolutions(
642            $this->testSession->getActiveId(),
643            $this->testSession->getPass()
644        );
645        $result = array();
646        $result['isAnswered'] = $state['authorized'];
647        $result['isAnswerChanged'] = $state['intermediate'];
648
649        echo json_encode($result);
650        exit;
651    }
652    // fau.
653
654    protected function submitIntermediateSolutionCmd()
655    {
656        $this->saveQuestionSolution(false, true);
657        // fau: testNav - set the 'answer changed' parameter when an intermediate solution is submitted
658        $this->setAnswerChangedParameter(true);
659        // fau.
660        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
661    }
662
663    /**
664     * Toggle side list
665     */
666    public function toggleSideListCmd()
667    {
668        global $DIC;
669        $ilUser = $DIC['ilUser'];
670
671        $show_side_list = $ilUser->getPref('side_list_of_questions');
672        $ilUser->writePref('side_list_of_questions', !$show_side_list);
673        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
674    }
675
676    protected function markQuestionAndSaveIntermediateCmd()
677    {
678        // fau: testNav - handle intermediate submit when marking the question
679        $this->handleIntermediateSubmit();
680        // fau.
681        $this->markQuestionCmd();
682    }
683
684    /**
685     * Set a question solved
686     */
687    protected function markQuestionCmd()
688    {
689        $questionId = $this->testSequence->getQuestionForSequence(
690            $this->getCurrentSequenceElement()
691        );
692
693        $this->object->setQuestionSetSolved(1, $questionId, $this->testSession->getUserId());
694
695        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
696    }
697
698    protected function unmarkQuestionAndSaveIntermediateCmd()
699    {
700        // fau: testNav - handle intermediate submit when unmarking the question
701        $this->handleIntermediateSubmit();
702        // fau.
703        $this->unmarkQuestionCmd();
704    }
705
706    /**
707     * Set a question unsolved
708     */
709    protected function unmarkQuestionCmd()
710    {
711        $questionId = $this->testSequence->getQuestionForSequence(
712            $this->getCurrentSequenceElement()
713        );
714
715        $this->object->setQuestionSetSolved(0, $questionId, $this->testSession->getUserId());
716
717        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
718    }
719
720    /**
721     * The final submission of a test was confirmed
722     */
723    protected function confirmFinishCmd()
724    {
725        $this->finishTestCmd(false);
726    }
727
728    /**
729     * Confirmation of the tests final submission
730     */
731    protected function confirmFinishTestCmd()
732    {
733        /**
734         * @var $ilUser ilObjUser
735         */
736        global $DIC;
737        $ilUser = $DIC['ilUser'];
738
739        require_once 'Services/Utilities/classes/class.ilConfirmationGUI.php';
740        $confirmation = new ilConfirmationGUI();
741        $confirmation->setFormAction($this->ctrl->getFormAction($this, 'confirmFinish'));
742        $confirmation->setHeaderText($this->lng->txt("tst_finish_confirmation_question"));
743        $confirmation->setConfirm($this->lng->txt("tst_finish_confirm_button"), 'confirmFinish');
744        $confirmation->setCancel($this->lng->txt("tst_finish_confirm_cancel_button"), ilTestPlayerCommands::BACK_FROM_FINISHING);
745
746        $this->populateHelperGuiContent($confirmation);
747    }
748
749    public function finishTestCmd($requires_confirmation = true)
750    {
751        unset($_SESSION["tst_next"]);
752
753        $active_id = $this->testSession->getActiveId();
754        $actualpass = ilObjTest::_getPass($active_id);
755
756        $allObligationsAnswered = ilObjTest::allObligationsAnswered($this->testSession->getTestId(), $active_id, $actualpass);
757
758        /*
759         * The following "endgames" are possible prior to actually finishing the test:
760         * - Obligations (Ability to finish the test.)
761         *      If not all obligatory questions are answered, the user is presented a list
762         *      showing the deficits.
763         * - Examview (Will to finish the test.)
764         *      With the examview, the participant can review all answers given in ILIAS or a PDF prior to
765         *      commencing to the finished test.
766         * - Last pass allowed (Reassuring the will to finish the test.)
767         *      If passes are limited, on the last pass, an additional confirmation is to be displayed.
768         */
769
770
771        if ($this->object->areObligationsEnabled() && !$allObligationsAnswered) {
772            if ($this->object->getListOfQuestions()) {
773                $this->ctrl->redirect($this, ilTestPlayerCommands::QUESTION_SUMMARY_INC_OBLIGATIONS);
774            } else {
775                $this->ctrl->redirect($this, ilTestPlayerCommands::QUESTION_SUMMARY_OBLIGATIONS_ONLY);
776            }
777
778            return;
779        }
780
781        // Examview enabled & !reviewed & requires_confirmation? test_submission_overview (review gui)
782        if ($this->object->getEnableExamview() && !isset($_GET['reviewed']) && $requires_confirmation) {
783            $this->ctrl->redirectByClass('ilTestSubmissionReviewGUI', "show");
784            return;
785        }
786
787        // Last try in limited tries & !confirmed
788        if (($requires_confirmation) && ($actualpass == $this->object->getNrOfTries() - 1)) {
789            // show confirmation page
790            return $this->confirmFinishTestCmd();
791        }
792
793        // Last try in limited tries & confirmed?
794        if (($actualpass == $this->object->getNrOfTries() - 1) && (!$requires_confirmation)) {
795            // @todo: php7 ask mister test
796            #$ilAuth->setIdle(ilSession::getIdleValue(), false);
797            #$ilAuth->setExpire(0);
798            switch ($this->object->getMailNotification()) {
799                case 1:
800                    $this->object->sendSimpleNotification($active_id);
801                    break;
802                case 2:
803                    $this->object->sendAdvancedNotification($active_id);
804                    break;
805            }
806        }
807
808        // Non-last try finish
809        if (!$_SESSION['tst_pass_finish']) {
810            if (!$_SESSION['tst_pass_finish']) {
811                $_SESSION['tst_pass_finish'] = 1;
812            }
813            if ($this->object->getMailNotificationType() == 1) {
814                switch ($this->object->getMailNotification()) {
815                    case 1:
816                        $this->object->sendSimpleNotification($active_id);
817                        break;
818                    case 2:
819                        $this->object->sendAdvancedNotification($active_id);
820                        break;
821                }
822            }
823        }
824
825        // no redirect request loops after test pass finished tasks has been performed
826
827        $this->performTestPassFinishedTasks($actualpass);
828
829        $this->ctrl->redirect($this, ilTestPlayerCommands::AFTER_TEST_PASS_FINISHED);
830    }
831
832    protected function performTestPassFinishedTasks($finishedPass)
833    {
834        require_once 'Modules/Test/classes/class.ilTestPassFinishTasks.php';
835
836        $finishTasks = new ilTestPassFinishTasks($this->testSession->getActiveId(), $this->object->getId());
837        $finishTasks->performFinishTasks($this->processLocker);
838    }
839
840    protected function afterTestPassFinishedCmd()
841    {
842        $activeId = $this->testSession->getActiveId();
843        $lastFinishedPass = $this->testSession->getLastFinishedPass();
844
845        // handle test signature
846        if ($this->isTestSignRedirectRequired($activeId, $lastFinishedPass)) {
847            $this->ctrl->redirectByClass('ilTestSignatureGUI', 'invokeSignaturePlugin');
848        }
849
850        // show final statement
851        if (!$_GET['skipfinalstatement']) {
852            if ($this->object->getShowFinalStatement()) {
853                $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_FINAL_STATMENT);
854            }
855        }
856
857        // redirect after test
858        $redirection_mode = $this->object->getRedirectionMode();
859        $redirection_url = $this->object->getRedirectionUrl();
860        if ($redirection_url && $redirection_mode) {
861            if ($redirection_mode == REDIRECT_KIOSK) {
862                if ($this->object->getKioskMode()) {
863                    ilUtil::redirect($redirection_url);
864                }
865            } else {
866                ilUtil::redirect($redirection_url);
867            }
868        }
869
870        // default redirect (pass overview when enabled, otherwise infoscreen)
871        $this->redirectBackCmd();
872    }
873
874    protected function isTestSignRedirectRequired($activeId, $lastFinishedPass)
875    {
876        if (!$this->object->getSignSubmission()) {
877            return false;
878        }
879
880        if (!is_null(ilSession::get("signed_{$activeId}_{$lastFinishedPass}"))) {
881            return false;
882        }
883
884        global $DIC;
885        $ilPluginAdmin = $DIC['ilPluginAdmin'];
886
887        $activePlugins = $ilPluginAdmin->getActivePluginsForSlot(IL_COMP_MODULE, 'Test', 'tsig');
888
889        if (!count($activePlugins)) {
890            return false;
891        }
892
893        return true;
894    }
895
896    /**
897     * @param $active
898     *
899     * @return void
900     */
901    protected function archiveParticipantSubmission($active, $pass)
902    {
903        global $DIC;
904        $ilObjDataCache = $DIC['ilObjDataCache'];
905
906        require_once 'Modules/Test/classes/class.ilTestResultHeaderLabelBuilder.php';
907        $testResultHeaderLabelBuilder = new ilTestResultHeaderLabelBuilder($this->lng, $ilObjDataCache);
908
909        $objectivesList = null;
910
911        if ($this->getObjectiveOrientedContainer()->isObjectiveOrientedPresentationRequired()) {
912            $testSequence = $this->testSequenceFactory->getSequenceByActiveIdAndPass($this->testSession->getActiveId(), $this->testSession->getPass());
913            $testSequence->loadFromDb();
914            $testSequence->loadQuestions();
915
916            require_once 'Modules/Course/classes/Objectives/class.ilLOTestQuestionAdapter.php';
917            $objectivesAdapter = ilLOTestQuestionAdapter::getInstance($this->testSession);
918
919            $objectivesList = $this->buildQuestionRelatedObjectivesList($objectivesAdapter, $testSequence);
920            $objectivesList->loadObjectivesTitles();
921
922            $testResultHeaderLabelBuilder->setObjectiveOrientedContainerId($this->testSession->getObjectiveOrientedContainerId());
923            $testResultHeaderLabelBuilder->setUserId($this->testSession->getUserId());
924            $testResultHeaderLabelBuilder->setTestObjId($this->object->getId());
925            $testResultHeaderLabelBuilder->setTestRefId($this->object->getRefId());
926            $testResultHeaderLabelBuilder->initObjectiveOrientedMode();
927        }
928
929        $results = $this->object->getTestResult(
930            $active,
931            $pass,
932            false,
933            !$this->getObjectiveOrientedContainer()->isObjectiveOrientedPresentationRequired()
934        );
935
936        require_once 'class.ilTestEvaluationGUI.php';
937        $testevaluationgui = new ilTestEvaluationGUI($this->object);
938        $results_output = $testevaluationgui->getPassListOfAnswers(
939            $results,
940            $active,
941            $pass,
942            false,
943            false,
944            false,
945            false,
946            false,
947            $objectivesList,
948            $testResultHeaderLabelBuilder
949        );
950
951        require_once './Modules/Test/classes/class.ilTestArchiver.php';
952        global $DIC;
953        $ilSetting = $DIC['ilSetting'];
954        $inst_id = $ilSetting->get('inst_id', null);
955        $archiver = new ilTestArchiver($this->object->getId());
956
957        $path = ilUtil::getWebspaceDir() . '/assessment/' . $this->object->getId() . '/exam_pdf';
958        if (!is_dir($path)) {
959            ilUtil::makeDirParents($path);
960        }
961        $filename = realpath($path) . '/exam_N' . $inst_id . '-' . $this->object->getId()
962                    . '-' . $active . '-' . $pass . '.pdf';
963
964        ilTestPDFGenerator::generatePDF($results_output, ilTestPDFGenerator::PDF_OUTPUT_FILE, $filename);
965        //$template->setVariable("PDF_FILE_LOCATION", $filename);
966        // Participant submission
967        $archiver->handInParticipantSubmission($active, $pass, $filename, $results_output);
968        //$archiver->handInParticipantMisc( $active, $pass, 'signature_gedoens.sig', $filename );
969        //$archiver->handInParticipantQuestionMaterial( $active, $pass, 123, 'file_upload.pdf', $filename );
970
971        global $DIC;
972        $ilias = $DIC['ilias'];
973        $questions = $this->object->getQuestions();
974        foreach ($questions as $question_id) {
975            $question_object = $this->object->getQuestionDataset($question_id);
976            if ($question_object->type_tag == 'assFileUpload') {
977                // Pfad: /data/default/assessment/tst_2/14/21/files/file_14_4_1370417414.png
978                // /data/ - klar
979                // /assessment/ - Konvention
980                // /tst_2/ = /tst_<test_id> (ilObjTest)
981                // /14/ = /<active_fi>/
982                // /21/ = /<question_id>/ (question_object)
983                // /files/ - Konvention
984                // file_14_4_1370417414.png = file_<active_fi>_<pass>_<some timestamp>.<ext>
985
986                $candidate_path =
987                    $ilias->ini_ilias->readVariable('server', 'absolute_path') . ilTestArchiver::DIR_SEP
988                        . $ilias->ini_ilias->readVariable('clients', 'path') . ilTestArchiver::DIR_SEP
989                        . $ilias->client_id . ilTestArchiver::DIR_SEP
990                        . 'assessment' . ilTestArchiver::DIR_SEP
991                        . 'tst_' . $this->object->test_id . ilTestArchiver::DIR_SEP
992                        . $active . ilTestArchiver::DIR_SEP
993                        . $question_id . ilTestArchiver::DIR_SEP
994                        . 'files' . ilTestArchiver::DIR_SEP;
995                $handle = opendir($candidate_path);
996                while ($handle !== false && ($file = readdir($handle)) !== false) {
997                    if ($file != null) {
998                        $filename_start = 'file_' . $active . '_' . $pass . '_';
999
1000                        if (strpos($file, $filename_start) === 0) {
1001                            $archiver->handInParticipantQuestionMaterial($active, $pass, $question_id, $file, $file);
1002                        }
1003                    }
1004                }
1005            }
1006        }
1007        $passdata = $this->object->getTestResult(
1008            $active,
1009            $pass,
1010            false,
1011            !$this->getObjectiveOrientedContainer()->isObjectiveOrientedPresentationRequired()
1012        );
1013        $overview = $testevaluationgui->getPassListOfAnswers(
1014            $passdata,
1015            $active,
1016            $pass,
1017            true,
1018            false,
1019            false,
1020            true,
1021            false,
1022            $objectivesList,
1023            $testResultHeaderLabelBuilder
1024        );
1025        $filename = realpath(ilUtil::getWebspaceDir()) . '/assessment/scores-' . $this->object->getId() . '-' . $active . '-' . $pass . '.pdf';
1026        ilTestPDFGenerator::generatePDF($overview, ilTestPDFGenerator::PDF_OUTPUT_FILE, $filename);
1027        $archiver->handInTestResult($active, $pass, $filename);
1028        unlink($filename);
1029
1030        return;
1031    }
1032
1033    public function redirectBackCmd()
1034    {
1035        global $DIC; /* @var ILIAS\DI\Container $DIC */
1036        require_once 'Modules/Test/classes/class.ilTestPassesSelector.php';
1037        $testPassesSelector = new ilTestPassesSelector($DIC['ilDB'], $this->object);
1038        $testPassesSelector->setActiveId($this->testSession->getActiveId());
1039        $testPassesSelector->setLastFinishedPass($this->testSession->getLastFinishedPass());
1040
1041        if (count($testPassesSelector->getReportablePasses())) {
1042            if ($this->getObjectiveOrientedContainer()->isObjectiveOrientedPresentationRequired()) {
1043                $this->ctrl->redirectByClass(array('ilTestResultsGUI', 'ilTestEvalObjectiveOrientedGUI'));
1044            }
1045
1046            $this->ctrl->redirectByClass(array('ilTestResultsGUI', 'ilMyTestResultsGUI', 'ilTestEvaluationGUI'));
1047        }
1048
1049        $this->backToInfoScreenCmd();
1050    }
1051
1052    protected function backToInfoScreenCmd()
1053    {
1054        $this->ctrl->redirectByClass('ilObjTestGUI', 'redirectToInfoScreen');
1055    }
1056
1057    /*
1058    * Presents the final statement of a test
1059    */
1060    public function showFinalStatementCmd()
1061    {
1062        $template = new ilTemplate("tpl.il_as_tst_final_statement.html", true, true, "Modules/Test");
1063        $this->ctrl->setParameter($this, "skipfinalstatement", 1);
1064        $template->setVariable("FORMACTION", $this->ctrl->getFormAction($this, ilTestPlayerCommands::AFTER_TEST_PASS_FINISHED));
1065        $template->setVariable("FINALSTATEMENT", $this->object->prepareTextareaOutput($this->object->getFinalStatement(), true));
1066        $template->setVariable("BUTTON_CONTINUE", $this->lng->txt("btn_next"));
1067        $this->tpl->setVariable($this->getContentBlockName(), $template->get());
1068    }
1069
1070    public function getKioskHead()
1071    {
1072        global $DIC;
1073        $ilUser = $DIC['ilUser'];
1074
1075        $template = new ilTemplate('tpl.il_as_tst_kiosk_head.html', true, true, 'Modules/Test');
1076        if ($this->object->getShowKioskModeTitle()) {
1077            $template->setCurrentBlock("kiosk_show_title");
1078            $template->setVariable("TEST_TITLE", $this->object->getTitle());
1079            $template->parseCurrentBlock();
1080        }
1081        if ($this->object->getShowKioskModeParticipant()) {
1082            $template->setCurrentBlock("kiosk_show_participant");
1083            $template->setVariable("PARTICIPANT_NAME_TXT", $this->lng->txt("login_as"));
1084            $template->setVariable("PARTICIPANT_NAME", $ilUser->getFullname());
1085            $template->setVariable("PARTICIPANT_LOGIN", $ilUser->getLogin());
1086            $template->setVariable("PARTICIPANT_MATRICULATION", $ilUser->getMatriculation());
1087            $template->setVariable("PARTICIPANT_EMAIL", $ilUser->getEmail());
1088            $template->parseCurrentBlock();
1089        }
1090        if ($this->object->isShowExamIdInTestPassEnabled()) {
1091            $exam_id = ilObjTest::buildExamId(
1092                $this->testSession->getActiveId(),
1093                $this->testSession->getPass(),
1094                $this->object->getId()
1095            );
1096
1097            $template->setCurrentBlock("kiosk_show_exam_id");
1098            $template->setVariable("EXAM_ID_TXT", $this->lng->txt("exam_id"));
1099            $template->setVariable("EXAM_ID", $exam_id);
1100            $template->parseCurrentBlock();
1101        }
1102        return $template->get();
1103    }
1104
1105    /**
1106     * @return string $formAction
1107     */
1108    protected function prepareTestPage($presentationMode, $sequenceElement, $questionId)
1109    {
1110        global $DIC;
1111        $ilUser = $DIC['ilUser'];
1112        $ilNavigationHistory = $DIC['ilNavigationHistory'];
1113
1114        $ilNavigationHistory->addItem(
1115            $this->testSession->getRefId(),
1116            $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::RESUME_PLAYER),
1117            'tst'
1118        );
1119
1120        $this->initTestPageTemplate();
1121        $this->populateContentStyleBlock();
1122        $this->populateSyntaxStyleBlock();
1123
1124        if ($this->isMaxProcessingTimeReached()) {
1125            $this->maxProcessingTimeReached();
1126            return;
1127        }
1128
1129        if ($this->object->endingTimeReached()) {
1130            $this->endingTimeReached();
1131            return;
1132        }
1133
1134        if ($this->isOptionalQuestionAnsweringConfirmationRequired($sequenceElement)) {
1135            $this->ctrl->setParameter($this, "sequence", $sequenceElement);
1136            $this->showAnswerOptionalQuestionsConfirmation();
1137            return;
1138        }
1139
1140        if ($this->object->getKioskMode()) {
1141            $this->populateKioskHead();
1142        }
1143
1144        $this->tpl->setVariable("TEST_ID", $this->object->getTestId());
1145        $this->tpl->setVariable("LOGIN", $ilUser->getLogin());
1146        $this->tpl->setVariable("SEQ_ID", $sequenceElement);
1147        $this->tpl->setVariable("QUEST_ID", $questionId);
1148
1149        if ($this->object->getEnableProcessingTime()) {
1150            $this->outProcessingTime($this->testSession->getActiveId());
1151        }
1152
1153        $this->tpl->setVariable("PAGETITLE", "- " . $this->object->getTitle());
1154
1155        if ($this->object->isShowExamIdInTestPassEnabled() && !$this->object->getKioskMode()) {
1156            $this->tpl->setCurrentBlock('exam_id_footer');
1157            $this->tpl->setVariable('EXAM_ID_VAL', ilObjTest::lookupExamId(
1158                $this->testSession->getActiveId(),
1159                $this->testSession->getPass(),
1160                $this->object->getId()
1161            ));
1162            $this->tpl->setVariable('EXAM_ID_TXT', $this->lng->txt('exam_id'));
1163            $this->tpl->parseCurrentBlock();
1164        }
1165
1166        if ($this->object->getListOfQuestions()) {
1167            $this->showSideList($presentationMode, $sequenceElement);
1168        }
1169    }
1170
1171    abstract protected function isOptionalQuestionAnsweringConfirmationRequired($sequenceElement);
1172
1173    abstract protected function isShowingPostponeStatusReguired($questionId);
1174
1175    protected function showQuestionViewable(assQuestionGUI $questionGui, $formAction, $isQuestionWorkedThrough, $instantResponse)
1176    {
1177        $questionNavigationGUI = $this->buildReadOnlyStateQuestionNavigationGUI($questionGui->object->getId());
1178        $questionNavigationGUI->setQuestionWorkedThrough($isQuestionWorkedThrough);
1179        $questionGui->setNavigationGUI($questionNavigationGUI);
1180
1181        // fau: testNav - set answere status in question header
1182        $questionGui->getQuestionHeaderBlockBuilder()->setQuestionAnswered($isQuestionWorkedThrough);
1183        // fau.
1184
1185        $answerFeedbackEnabled = (
1186            $instantResponse && $this->object->getSpecificAnswerFeedback()
1187        );
1188
1189        $solutionoutput = $questionGui->getSolutionOutput(
1190            $this->testSession->getActiveId(), 	#active_id
1191            $this->testSession->getPass(),		#pass
1192            false, 								#graphical_output
1193            false,								#result_output
1194            true, 								#show_question_only
1195            $answerFeedbackEnabled,				#show_feedback
1196            false, 								#show_correct_solution
1197            false, 								#show_manual_scoring
1198            true								#show_question_text
1199        );
1200
1201        $pageoutput = $questionGui->outQuestionPage(
1202            "",
1203            $this->isShowingPostponeStatusReguired($questionGui->object->getId()),
1204            $this->testSession->getActiveId(),
1205            $solutionoutput
1206        );
1207
1208        $this->tpl->setCurrentBlock('readonly_css_class');
1209        $this->tpl->touchBlock('readonly_css_class');
1210        $this->tpl->parseCurrentBlock();
1211
1212        $this->tpl->setVariable('QUESTION_OUTPUT', $pageoutput);
1213
1214        $this->tpl->setVariable("FORMACTION", $formAction);
1215        $this->tpl->setVariable("ENCTYPE", 'enctype="' . $questionGui->getFormEncodingType() . '"');
1216        $this->tpl->setVariable("FORM_TIMESTAMP", time());
1217    }
1218
1219    protected function showQuestionEditable(assQuestionGUI $questionGui, $formAction, $isQuestionWorkedThrough, $instantResponse)
1220    {
1221        $questionNavigationGUI = $this->buildEditableStateQuestionNavigationGUI(
1222            $questionGui->object->getId(),
1223            $this->populateCharSelectorIfRequired()
1224        );
1225        if ($isQuestionWorkedThrough) {
1226            $questionNavigationGUI->setDiscardSolutionButtonEnabled(true);
1227            // fau: testNav - set answere status in question header
1228            $questionGui->getQuestionHeaderBlockBuilder()->setQuestionAnswered(true);
1229        // fau.
1230        } elseif ($this->object->isPostponingEnabled()) {
1231            $questionNavigationGUI->setSkipQuestionLinkTarget(
1232                $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::SKIP_QUESTION)
1233            );
1234        }
1235        $questionGui->setNavigationGUI($questionNavigationGUI);
1236
1237        $isPostponed = $this->isShowingPostponeStatusReguired($questionGui->object->getId());
1238
1239        $answerFeedbackEnabled = (
1240            $instantResponse && $this->object->getSpecificAnswerFeedback()
1241        );
1242
1243        if (isset($_GET['save_error']) && $_GET['save_error'] == 1 && isset($_SESSION['previouspost'])) {
1244            $userPostSolution = $_SESSION['previouspost'];
1245            unset($_SESSION['previouspost']);
1246        } else {
1247            $userPostSolution = false;
1248        }
1249
1250        // fau: testNav - add special checkbox for mc question
1251        // moved to another patch block
1252        // fau.
1253
1254        // hey: prevPassSolutions - determine solution pass index and configure gui accordingly
1255        $qstConfig = $questionGui->object->getTestPresentationConfig();
1256
1257        if ($questionGui instanceof assMultipleChoiceGUI) {
1258            $qstConfig->setWorkedThrough($isQuestionWorkedThrough);
1259            $qstConfig->setIsUnchangedAnswerPossible($this->object->getMCScoring());
1260        }
1261
1262        if ($qstConfig->isPreviousPassSolutionReuseAllowed()) {
1263            $passIndex = $this->determineSolutionPassIndex($questionGui); // last pass having solution stored
1264            if ($passIndex < $this->testSession->getPass()) { // it's the previous pass if current pass is higher
1265                $qstConfig->setSolutionInitiallyPrefilled(true);
1266            }
1267        } else {
1268            $passIndex = $this->testSession->getPass();
1269        }
1270        // hey.
1271
1272        // Answer specific feedback is rendered into the display of the test question with in the concrete question types outQuestionForTest-method.
1273        // Notation of the params prior to getting rid of this crap in favor of a class
1274        $questionGui->outQuestionForTest(
1275            $formAction, 							#form_action
1276            $this->testSession->getActiveId(),		#active_id
1277            // hey: prevPassSolutions - prepared pass index having no, current or previous solution
1278            $passIndex, 							#pass
1279            // hey.
1280            $isPostponed, 							#is_postponed
1281            $userPostSolution, 						#user_post_solution
1282            $answerFeedbackEnabled					#answer_feedback == inline_specific_feedback
1283        );
1284        // The display of specific inline feedback and specific feedback in an own block is to honor questions, which
1285        // have the possibility to embed the specific feedback into their output while maintaining compatibility to
1286        // questions, which do not have such facilities. E.g. there can be no "specific inline feedback" for essay
1287        // questions, while the multiple-choice questions do well.
1288
1289
1290        $this->populateModals();
1291
1292        // fau: testNav - pouplate the new question edit control instead of the deprecated intermediate solution saver
1293        $this->populateQuestionEditControl($questionGui);
1294        // fau.
1295    }
1296
1297    // hey: prevPassSolutions - determine solution pass index
1298    protected function determineSolutionPassIndex(assQuestionGUI $questionGui)
1299    {
1300        require_once './Modules/Test/classes/class.ilObjTest.php';
1301
1302        if (ilObjTest::_getUsePreviousAnswers($this->testSession->getActiveId(), true)) {
1303            $currentSolutionAvailable = $questionGui->object->authorizedOrIntermediateSolutionExists(
1304                $this->testSession->getActiveId(),
1305                $this->testSession->getPass()
1306            );
1307
1308            if (!$currentSolutionAvailable) {
1309                $previousPass = $questionGui->object->getSolutionMaxPass(
1310                    $this->testSession->getActiveId()
1311                );
1312
1313                $previousSolutionAvailable = $questionGui->object->authorizedSolutionExists(
1314                    $this->testSession->getActiveId(),
1315                    $previousPass
1316                );
1317
1318                if ($previousSolutionAvailable) {
1319                    return $previousPass;
1320                }
1321            }
1322        }
1323
1324        return $this->testSession->getPass();
1325    }
1326    // hey.
1327
1328    abstract protected function showQuestionCmd();
1329
1330    abstract protected function editSolutionCmd();
1331
1332    abstract protected function submitSolutionCmd();
1333
1334    // fau: testNav - new function to revert probably auto-saved changes and show the last submitted question state
1335    protected function revertChangesCmd()
1336    {
1337        $this->removeIntermediateSolution();
1338        $this->setAnswerChangedParameter(false);
1339        $this->ctrl->saveParameter($this, 'sequence');
1340        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1341    }
1342    // fau.
1343
1344    abstract protected function discardSolutionCmd();
1345
1346    abstract protected function skipQuestionCmd();
1347
1348    abstract protected function startTestCmd();
1349    /**
1350    * check access restrictions like client ip, partipating user etc.
1351    *
1352    * check access restrictions like client ip, partipating user etc.
1353    *
1354    * @access public
1355    */
1356    public function checkOnlineTestAccess()
1357    {
1358        global $DIC;
1359        $ilUser = $DIC['ilUser'];
1360
1361        // check if user is invited to participate
1362        $user = $this->object->getInvitedUsers($ilUser->getId());
1363        if (!is_array($user) || count($user) != 1) {
1364            ilUtil::sendInfo($this->lng->txt("user_not_invited"), true);
1365            $this->ctrl->redirectByClass("ilobjtestgui", "backToRepository");
1366        }
1367
1368        $user = array_pop($user);
1369        // check if client ip is set and if current remote addr is equal to stored client-ip
1370        if (strcmp($user["clientip"], "") != 0 && strcmp($user["clientip"], $_SERVER["REMOTE_ADDR"]) != 0) {
1371            ilUtil::sendInfo($this->lng->txt("user_wrong_clientip"), true);
1372            $this->ctrl->redirectByClass("ilobjtestgui", "backToRepository");
1373        }
1374    }
1375
1376
1377    /**
1378     * test accessible returns true if the user can perform the test
1379     */
1380    public function isTestAccessible()
1381    {
1382        return 	!$this->isNrOfTriesReached()
1383                and !$this->isMaxProcessingTimeReached()
1384                and $this->object->startingTimeReached()
1385                and !$this->object->endingTimeReached();
1386    }
1387
1388    /**
1389     * nr of tries exceeded
1390     */
1391    public function isNrOfTriesReached()
1392    {
1393        return $this->object->hasNrOfTriesRestriction() && $this->object->isNrOfTriesReached($this->testSession->getPass());
1394    }
1395
1396    /**
1397     * handle endingTimeReached
1398     * @private
1399     */
1400
1401    public function endingTimeReached()
1402    {
1403        ilUtil::sendInfo(sprintf($this->lng->txt("detail_ending_time_reached"), ilDatePresentation::formatDate(new ilDateTime($this->object->getEndingTime(), IL_CAL_UNIX))));
1404        $this->testSession->increasePass();
1405        $this->testSession->setLastSequence(0);
1406        $this->testSession->saveToDb();
1407
1408        $this->redirectBackCmd();
1409    }
1410
1411    /**
1412    * Outputs a message when the maximum processing time is reached
1413    *
1414    * Outputs a message when the maximum processing time is reached
1415    *
1416    * @access public
1417    */
1418    public function maxProcessingTimeReached()
1419    {
1420        $this->suspendTestCmd();
1421    }
1422
1423    /**
1424    * confirm submit results
1425    * if confirm then results are submitted and the screen will be redirected to the startpage of the test
1426    * @access public
1427    */
1428    public function confirmSubmitAnswers()
1429    {
1430        $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_submit_answers_confirm.html", "Modules/Test");
1431        $this->tpl->setCurrentBlock("adm_content");
1432        if ($this->object->isTestFinished($this->testSession->getActiveId())) {
1433            $this->tpl->setCurrentBlock("not_submit_allowed");
1434            $this->tpl->setVariable("TEXT_ALREADY_SUBMITTED", $this->lng->txt("tst_already_submitted"));
1435            $this->tpl->setVariable("BTN_OK", $this->lng->txt("tst_show_answer_sheet"));
1436        } else {
1437            $this->tpl->setCurrentBlock("submit_allowed");
1438            $this->tpl->setVariable("TEXT_CONFIRM_SUBMIT_RESULTS", $this->lng->txt("tst_confirm_submit_answers"));
1439            $this->tpl->setVariable("BTN_OK", $this->lng->txt("tst_submit_results"));
1440        }
1441        $this->tpl->setVariable("BTN_BACK", $this->lng->txt("back"));
1442        $this->tpl->setVariable("FORMACTION", $this->ctrl->getFormAction($this, "finalSubmission"));
1443        $this->tpl->parseCurrentBlock();
1444    }
1445
1446    public function outProcessingTime($active_id)
1447    {
1448        global $DIC;
1449        $ilUser = $DIC['ilUser'];
1450
1451        $starting_time = $this->object->getStartingTimeOfUser($active_id);
1452        $processing_time = $this->object->getProcessingTimeInSeconds($active_id);
1453        $processing_time_minutes = floor($processing_time / 60);
1454        $processing_time_seconds = $processing_time - $processing_time_minutes * 60;
1455        $str_processing_time = "";
1456        if ($processing_time_minutes > 0) {
1457            $str_processing_time = $processing_time_minutes . " " . ($processing_time_minutes == 1 ? $this->lng->txt("minute") : $this->lng->txt("minutes"));
1458        }
1459        if ($processing_time_seconds > 0) {
1460            if (strlen($str_processing_time) > 0) {
1461                $str_processing_time .= " " . $this->lng->txt("and") . " ";
1462            }
1463            $str_processing_time .= $processing_time_seconds . " " . ($processing_time_seconds == 1 ? $this->lng->txt("second") : $this->lng->txt("seconds"));
1464        }
1465        $time_left = $starting_time + $processing_time - time();
1466        $time_left_minutes = floor($time_left / 60);
1467        $time_left_seconds = $time_left - $time_left_minutes * 60;
1468        $str_time_left = "";
1469        if ($time_left_minutes > 0) {
1470            $str_time_left = $time_left_minutes . " " . ($time_left_minutes == 1 ? $this->lng->txt("minute") : $this->lng->txt("minutes"));
1471        }
1472        if ($time_left < 300) {
1473            if ($time_left_seconds > 0) {
1474                if (strlen($str_time_left) > 0) {
1475                    $str_time_left .= " " . $this->lng->txt("and") . " ";
1476                }
1477                $str_time_left .= $time_left_seconds . " " . ($time_left_seconds == 1 ? $this->lng->txt("second") : $this->lng->txt("seconds"));
1478            }
1479        }
1480        $date = getdate($starting_time);
1481        $formattedStartingTime = ilDatePresentation::formatDate(new ilDateTime($date, IL_CAL_FKT_GETDATE));
1482        $datenow = getdate();
1483        $this->tpl->setCurrentBlock("enableprocessingtime");
1484        $this->tpl->setVariable(
1485            "USER_WORKING_TIME",
1486            sprintf(
1487                $this->lng->txt("tst_time_already_spent"),
1488                $formattedStartingTime,
1489                $str_processing_time
1490            )
1491        );
1492        $this->tpl->setVariable("USER_REMAINING_TIME", sprintf($this->lng->txt("tst_time_already_spent_left"), $str_time_left));
1493        $this->tpl->parseCurrentBlock();
1494
1495        // jQuery is required by tpl.workingtime.js
1496        require_once "./Services/jQuery/classes/class.iljQueryUtil.php";
1497        iljQueryUtil::initjQuery();
1498        $template = new ilTemplate("tpl.workingtime.js", true, true, 'Modules/Test');
1499        $template->setVariable("STRING_MINUTE", $this->lng->txt("minute"));
1500        $template->setVariable("STRING_MINUTES", $this->lng->txt("minutes"));
1501        $template->setVariable("STRING_SECOND", $this->lng->txt("second"));
1502        $template->setVariable("STRING_SECONDS", $this->lng->txt("seconds"));
1503        $template->setVariable("STRING_TIMELEFT", $this->lng->txt("tst_time_already_spent_left"));
1504        $template->setVariable("AND", strtolower($this->lng->txt("and")));
1505        $template->setVariable("YEAR", $date["year"]);
1506        $template->setVariable("MONTH", $date["mon"] - 1);
1507        $template->setVariable("DAY", $date["mday"]);
1508        $template->setVariable("HOUR", $date["hours"]);
1509        $template->setVariable("MINUTE", $date["minutes"]);
1510        $template->setVariable("SECOND", $date["seconds"]);
1511        if ($this->object->isEndingTimeEnabled()) {
1512            $date_time = new ilDateTime($this->object->getEndingTime(), IL_CAL_UNIX);
1513            preg_match("/(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})/", $date_time->get(IL_CAL_TIMESTAMP), $matches);
1514            $template->setVariable("ENDYEAR", $matches[1]);
1515            $template->setVariable("ENDMONTH", $matches[2] - 1);
1516            $template->setVariable("ENDDAY", $matches[3]);
1517            $template->setVariable("ENDHOUR", $matches[4]);
1518            $template->setVariable("ENDMINUTE", $matches[5]);
1519            $template->setVariable("ENDSECOND", $matches[6]);
1520        }
1521        $template->setVariable("YEARNOW", $datenow["year"]);
1522        $template->setVariable("MONTHNOW", $datenow["mon"] - 1);
1523        $template->setVariable("DAYNOW", $datenow["mday"]);
1524        $template->setVariable("HOURNOW", $datenow["hours"]);
1525        $template->setVariable("MINUTENOW", $datenow["minutes"]);
1526        $template->setVariable("SECONDNOW", $datenow["seconds"]);
1527        $template->setVariable("PTIME_M", $processing_time_minutes);
1528        $template->setVariable("PTIME_S", $processing_time_seconds);
1529        if ($this->ctrl->getCmd() == 'outQuestionSummary') {
1530            $template->setVariable("REDIRECT_URL", $this->ctrl->getFormAction($this, 'redirectAfterDashboard'));
1531        } else {
1532            $template->setVariable("REDIRECT_URL", "");
1533        }
1534        $this->tpl->addOnLoadCode($template->get());
1535    }
1536
1537    protected function showSideList($presentationMode, $currentSequenceElement)
1538    {
1539        global $DIC;
1540        $ilUser = $DIC['ilUser'];
1541
1542        $sideListActive = $ilUser->getPref('side_list_of_questions');
1543
1544        if ($sideListActive) {
1545            $this->tpl->addCss(
1546                ilUtil::getStyleSheetLocation("output", "ta_split.css", "Modules/Test"),
1547                "screen"
1548            );
1549
1550            $questionSummaryData = $this->service->getQuestionSummaryData($this->testSequence, false);
1551
1552            require_once 'Modules/Test/classes/class.ilTestQuestionSideListGUI.php';
1553            $questionSideListGUI = new ilTestQuestionSideListGUI($this->ctrl, $this->lng);
1554            $questionSideListGUI->setTargetGUI($this);
1555            $questionSideListGUI->setQuestionSummaryData($questionSummaryData);
1556            $questionSideListGUI->setCurrentSequenceElement($currentSequenceElement);
1557            // fau: testNav - set side list presentation mode to "view" to allow navigation when question is in edit mode
1558            $questionSideListGUI->setCurrentPresentationMode(ilTestPlayerAbstractGUI::PRESENTATION_MODE_VIEW);
1559            $questionSideListGUI->setDisabled(false);
1560            //			$questionSideListGUI->setCurrentPresentationMode($presentationMode);
1561            //			$questionSideListGUI->setDisabled($presentationMode == self::PRESENTATION_MODE_EDIT);
1562            // fau.
1563            $this->tpl->setVariable('LIST_OF_QUESTIONS', $questionSideListGUI->getHTML());
1564        }
1565    }
1566
1567    abstract protected function isQuestionSummaryFinishTestButtonRequired();
1568
1569    /**
1570     * Output of a summary of all test questions for test participants
1571     */
1572    public function outQuestionSummaryCmd($fullpage = true, $contextFinishTest = false, $obligationsInfo = false, $obligationsFilter = false)
1573    {
1574        if ($fullpage) {
1575            $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_question_summary.html", "Modules/Test");
1576        }
1577
1578        $obligationsFulfilled = \ilObjTest::allObligationsAnswered(
1579            $this->object->getId(),
1580            $this->testSession->getActiveId(),
1581            $this->testSession->getPass()
1582        );
1583
1584        if ($obligationsInfo && $this->object->areObligationsEnabled() && !$obligationsFulfilled) {
1585            ilUtil::sendFailure($this->lng->txt('not_all_obligations_answered'));
1586        }
1587
1588        if ($this->object->getKioskMode() && $fullpage) {
1589            $head = $this->getKioskHead();
1590            if (strlen($head)) {
1591                $this->tpl->setCurrentBlock("kiosk_options");
1592                $this->tpl->setVariable("KIOSK_HEAD", $head);
1593                $this->tpl->parseCurrentBlock();
1594            }
1595        }
1596
1597
1598        $active_id = $this->testSession->getActiveId();
1599        $questionSummaryData = $this->service->getQuestionSummaryData($this->testSequence, $obligationsFilter);
1600
1601        $this->ctrl->setParameter($this, "sequence", $_GET["sequence"]);
1602
1603        if ($fullpage) {
1604            include_once "./Modules/Test/classes/tables/class.ilListOfQuestionsTableGUI.php";
1605            $table_gui = new ilListOfQuestionsTableGUI($this, 'showQuestion');
1606
1607            $table_gui->setShowPointsEnabled(!$this->object->getTitleOutput());
1608            $table_gui->setShowMarkerEnabled($this->object->getShowMarker());
1609            $table_gui->setObligationsNotAnswered(!$obligationsFulfilled);
1610            $table_gui->setShowObligationsEnabled($this->object->areObligationsEnabled());
1611            $table_gui->setObligationsFilterEnabled($obligationsFilter);
1612            $table_gui->setFinishTestButtonEnabled($this->isQuestionSummaryFinishTestButtonRequired());
1613
1614            $table_gui->init();
1615
1616            $table_gui->setData($questionSummaryData);
1617
1618            $this->tpl->setVariable('TABLE_LIST_OF_QUESTIONS', $table_gui->getHTML());
1619
1620            if ($this->object->getEnableProcessingTime()) {
1621                $this->outProcessingTime($active_id);
1622            }
1623
1624            if ($this->object->isShowExamIdInTestPassEnabled()) {
1625                $this->tpl->setCurrentBlock('exam_id_footer');
1626                $this->tpl->setVariable('EXAM_ID_VAL', ilObjTest::lookupExamId(
1627                    $this->testSession->getActiveId(),
1628                    $this->testSession->getPass(),
1629                    $this->object->getId()
1630                ));
1631                $this->tpl->setVariable('EXAM_ID_TXT', $this->lng->txt('exam_id'));
1632                $this->tpl->parseCurrentBlock();
1633            }
1634        }
1635    }
1636
1637    public function outQuestionSummaryWithObligationsInfoCmd()
1638    {
1639        return $this->outQuestionSummaryCmd(true, true, true, false);
1640    }
1641
1642    public function outObligationsOnlySummaryCmd()
1643    {
1644        return $this->outQuestionSummaryCmd(true, true, true, true);
1645    }
1646
1647    public function showMaximumAllowedUsersReachedMessage()
1648    {
1649        $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_max_allowed_users_reached.html", "Modules/Test");
1650        $this->tpl->setCurrentBlock("adm_content");
1651        $this->tpl->setVariable("MAX_ALLOWED_USERS_MESSAGE", sprintf($this->lng->txt("tst_max_allowed_users_message"), $this->object->getAllowedUsersTimeGap()));
1652        $this->tpl->setVariable("MAX_ALLOWED_USERS_HEADING", sprintf($this->lng->txt("tst_max_allowed_users_heading"), $this->object->getAllowedUsersTimeGap()));
1653        $this->tpl->setVariable("CMD_BACK_TO_INFOSCREEN", ilTestPlayerCommands::BACK_TO_INFO_SCREEN);
1654        $this->tpl->setVariable("TXT_BACK_TO_INFOSCREEN", $this->lng->txt("tst_results_back_introduction"));
1655        $this->tpl->setVariable("FORMACTION", $this->ctrl->getFormAction($this));
1656        $this->tpl->parseCurrentBlock();
1657    }
1658
1659    public function backFromFinishingCmd()
1660    {
1661        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
1662    }
1663
1664    /**
1665    * Creates an output of the solution of an answer compared to the correct solution
1666    *
1667    * @access public
1668    */
1669    public function outCorrectSolution()
1670    {
1671        $this->tpl->addBlockFile("ADM_CONTENT", "adm_content", "tpl.il_as_tst_correct_solution.html", "Modules/Test");
1672
1673        include_once("./Services/Style/Content/classes/class.ilObjStyleSheet.php");
1674        $this->tpl->setCurrentBlock("ContentStyle");
1675        $this->tpl->setVariable("LOCATION_CONTENT_STYLESHEET", ilObjStyleSheet::getContentStylePath(0));
1676        $this->tpl->parseCurrentBlock();
1677
1678        $this->tpl->setCurrentBlock("SyntaxStyle");
1679        $this->tpl->setVariable("LOCATION_SYNTAX_STYLESHEET", ilObjStyleSheet::getSyntaxStylePath());
1680        $this->tpl->parseCurrentBlock();
1681
1682        $this->tpl->addCss(ilUtil::getStyleSheetLocation("output", "test_print.css", "Modules/Test"), "print");
1683        if ($this->object->getShowSolutionAnswersOnly()) {
1684            $this->tpl->addCss(ilUtil::getStyleSheetLocation("output", "test_print_hide_content.css", "Modules/Test"), "print");
1685        }
1686
1687        $this->tpl->setCurrentBlock("adm_content");
1688        $solution = $this->getCorrectSolutionOutput($_GET["evaluation"], $_GET["active_id"], $_GET["pass"]);
1689        $this->tpl->setVariable("OUTPUT_SOLUTION", $solution);
1690        $this->tpl->setVariable("TEXT_BACK", $this->lng->txt("back"));
1691        $this->ctrl->saveParameter($this, "pass");
1692        $this->ctrl->saveParameter($this, "active_id");
1693        $this->tpl->setVariable("URL_BACK", $this->ctrl->getLinkTarget($this, "outUserResultsOverview"));
1694        $this->tpl->parseCurrentBlock();
1695    }
1696
1697    /**
1698    * Creates an output of the list of answers for a test participant during the test
1699    * (only the actual pass will be shown)
1700    *
1701    * @param integer $active_id Active id of the participant
1702    * @param integer $pass Test pass of the participant
1703    * @param boolean $testnavigation Deceides wheather to show a navigation for tests or not
1704    * @access public
1705    */
1706    public function showListOfAnswers($active_id, $pass = null, $top_data = "", $bottom_data = "")
1707    {
1708        global $DIC;
1709        $ilUser = $DIC['ilUser'];
1710
1711        $this->tpl->addBlockFile($this->getContentBlockName(), "adm_content", "tpl.il_as_tst_finish_list_of_answers.html", "Modules/Test");
1712
1713        $result_array = &$this->object->getTestResult(
1714            $active_id,
1715            $pass,
1716            false,
1717            !$this->getObjectiveOrientedContainer()->isObjectiveOrientedPresentationRequired()
1718        );
1719
1720        $counter = 1;
1721        // output of questions with solutions
1722        foreach ($result_array as $question_data) {
1723            $question = $question_data["qid"];
1724            if (is_numeric($question)) {
1725                $this->tpl->setCurrentBlock("printview_question");
1726                $question_gui = $this->object->createQuestionGUI("", $question);
1727                $template = new ilTemplate("tpl.il_as_qpl_question_printview.html", true, true, "Modules/TestQuestionPool");
1728                $template->setVariable("COUNTER_QUESTION", $counter . ". ");
1729                $template->setVariable("QUESTION_TITLE", $question_gui->object->getTitle());
1730
1731                $show_question_only = ($this->object->getShowSolutionAnswersOnly()) ? true : false;
1732                $result_output = $question_gui->getSolutionOutput($active_id, $pass, false, false, $show_question_only, $this->object->getShowSolutionFeedback());
1733                $template->setVariable("SOLUTION_OUTPUT", $result_output);
1734                $this->tpl->setVariable("QUESTION_OUTPUT", $template->get());
1735                $this->tpl->parseCurrentBlock();
1736                $counter++;
1737            }
1738        }
1739
1740        $this->tpl->addCss(ilUtil::getStyleSheetLocation("output", "test_print.css", "Modules/Test"), "print");
1741        if ($this->object->getShowSolutionAnswersOnly()) {
1742            $this->tpl->addCss(ilUtil::getStyleSheetLocation("output", "test_print_hide_content.css", "Modules/Test"), "print");
1743        }
1744        if (strlen($top_data)) {
1745            $this->tpl->setCurrentBlock("top_data");
1746            $this->tpl->setVariable("TOP_DATA", $top_data);
1747            $this->tpl->parseCurrentBlock();
1748        }
1749
1750        if (strlen($bottom_data)) {
1751            $this->tpl->setCurrentBlock("bottom_data");
1752            $this->tpl->setVariable("FORMACTION", $this->ctrl->getFormAction($this));
1753            $this->tpl->setVariable("BOTTOM_DATA", $bottom_data);
1754            $this->tpl->parseCurrentBlock();
1755        }
1756
1757        $this->tpl->setCurrentBlock("adm_content");
1758        $this->tpl->setVariable("TXT_ANSWER_SHEET", $this->lng->txt("tst_list_of_answers"));
1759        $user_data = $this->getAdditionalUsrDataHtmlAndPopulateWindowTitle($this->testSession, $active_id, true);
1760        $signature = $this->getResultsSignature();
1761        $this->tpl->setVariable("USER_DETAILS", $user_data);
1762        $this->tpl->setVariable("SIGNATURE", $signature);
1763        $this->tpl->setVariable("TITLE", $this->object->getTitle());
1764        $this->tpl->setVariable("TXT_TEST_PROLOG", $this->lng->txt("tst_your_answers"));
1765        $invited_user = &$this->object->getInvitedUsers($ilUser->getId());
1766        $pagetitle = $this->object->getTitle() . " - " . $this->lng->txt("clientip") .
1767            ": " . $invited_user[$ilUser->getId()]["clientip"] . " - " .
1768            $this->lng->txt("matriculation") . ": " .
1769            $invited_user[$ilUser->getId()]["matriculation"];
1770        $this->tpl->setVariable("PAGETITLE", $pagetitle);
1771        $this->tpl->parseCurrentBlock();
1772    }
1773
1774    /**
1775     * Returns the name of the current content block (depends on the kiosk mode setting)
1776     *
1777     * @return string The name of the content block
1778     */
1779    public function getContentBlockName()
1780    {
1781        return "ADM_CONTENT";
1782
1783        if ($this->object->getKioskMode()) {
1784            $this->tpl->setBodyClass("kiosk");
1785            $this->tpl->hideFooter();
1786            return "CONTENT";
1787        } else {
1788            return "ADM_CONTENT";
1789        }
1790    }
1791
1792    public function outUserResultsOverviewCmd()
1793    {
1794        $this->ctrl->redirectByClass(
1795            array('ilRepositoryGUI', 'ilObjTestGUI', 'ilTestEvaluationGUI'),
1796            "outUserResultsOverview"
1797        );
1798    }
1799
1800    /**
1801     * Go to requested hint list
1802     */
1803    protected function showRequestedHintListCmd()
1804    {
1805        // fau: testNav - handle intermediate submit for viewing requested hints
1806        $this->handleIntermediateSubmit();
1807        // fau.
1808
1809        $this->ctrl->setParameter($this, 'pmode', self::PRESENTATION_MODE_EDIT);
1810
1811        require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintRequestGUI.php';
1812        $this->ctrl->redirectByClass('ilAssQuestionHintRequestGUI', ilAssQuestionHintRequestGUI::CMD_SHOW_LIST);
1813    }
1814
1815    /**
1816     * Go to hint request confirmation
1817     */
1818    protected function confirmHintRequestCmd()
1819    {
1820        // fau: testNav - handle intermediate submit for confirming hint requests
1821        $this->handleIntermediateSubmit();
1822        // fau.
1823
1824        $this->ctrl->setParameter($this, 'pmode', self::PRESENTATION_MODE_EDIT);
1825
1826        require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintRequestGUI.php';
1827        $this->ctrl->redirectByClass('ilAssQuestionHintRequestGUI', ilAssQuestionHintRequestGUI::CMD_CONFIRM_REQUEST);
1828    }
1829
1830    abstract protected function isFirstQuestionInSequence($sequenceElement);
1831
1832    abstract protected function isLastQuestionInSequence($sequenceElement);
1833
1834
1835    abstract protected function handleQuestionActionCmd();
1836
1837    abstract protected function showInstantResponseCmd();
1838
1839    abstract protected function nextQuestionCmd();
1840
1841    abstract protected function previousQuestionCmd();
1842
1843    protected function prepareSummaryPage()
1844    {
1845        $this->tpl->addBlockFile(
1846            $this->getContentBlockName(),
1847            'adm_content',
1848            'tpl.il_as_tst_question_summary.html',
1849            'Modules/Test'
1850        );
1851
1852        if ($this->object->getKioskMode()) {
1853            $this->populateKioskHead();
1854        }
1855    }
1856
1857    protected function initTestPageTemplate()
1858    {
1859        $this->tpl->addBlockFile(
1860            $this->getContentBlockName(),
1861            'adm_content',
1862            'tpl.il_as_tst_output.html',
1863            'Modules/Test'
1864        );
1865    }
1866
1867    protected function populateKioskHead()
1868    {
1869        ilUtil::sendInfo(); // ???
1870
1871        $head = $this->getKioskHead();
1872
1873        if (strlen($head)) {
1874            $this->tpl->setCurrentBlock("kiosk_options");
1875            $this->tpl->setVariable("KIOSK_HEAD", $head);
1876            $this->tpl->parseCurrentBlock();
1877        }
1878    }
1879
1880    protected function handlePasswordProtectionRedirect()
1881    {
1882        if ($this->ctrl->getNextClass() == 'iltestpasswordprotectiongui') {
1883            return;
1884        }
1885
1886        if (!$this->passwordChecker->isPasswordProtectionPageRedirectRequired()) {
1887            return;
1888        }
1889
1890        $this->ctrl->setParameter($this, 'lock', $this->getLockParameter());
1891
1892        $nextCommand = $this->ctrl->getCmdClass() . '::' . $this->ctrl->getCmd();
1893        $this->ctrl->setParameterByClass('ilTestPasswordProtectionGUI', 'nextCommand', $nextCommand);
1894        $this->ctrl->redirectByClass('ilTestPasswordProtectionGUI', 'showPasswordForm');
1895    }
1896
1897    protected function isParticipantsAnswerFixed($questionId)
1898    {
1899        if ($this->object->isInstantFeedbackAnswerFixationEnabled() && $this->testSequence->isQuestionChecked($questionId)) {
1900            return true;
1901        }
1902
1903        if ($this->object->isFollowupQuestionAnswerFixationEnabled() && $this->testSequence->isNextQuestionPresented($questionId)) {
1904            return true;
1905        }
1906
1907        return false;
1908    }
1909
1910    /**
1911     * @return string
1912     */
1913    protected function getIntroductionPageButtonLabel()
1914    {
1915        return $this->lng->txt("save_introduction");
1916    }
1917
1918    protected function initAssessmentSettings()
1919    {
1920        $this->assSettings = new ilSetting('assessment');
1921    }
1922
1923    /**
1924     * @param ilTestSession $testSession
1925     */
1926    protected function handleSkillTriggering(ilTestSession $testSession)
1927    {
1928        $questionList = $this->buildTestPassQuestionList();
1929        $questionList->load();
1930
1931        $testResults = $this->object->getTestResult($testSession->getActiveId(), $testSession->getPass(), true);
1932
1933        require_once 'Modules/Test/classes/class.ilTestSkillEvaluation.php';
1934        $skillEvaluation = new ilTestSkillEvaluation($this->db, $this->object->getTestId(), $this->object->getRefId());
1935
1936        $skillEvaluation->setUserId($testSession->getUserId());
1937        $skillEvaluation->setActiveId($testSession->getActiveId());
1938        $skillEvaluation->setPass($testSession->getPass());
1939
1940        $skillEvaluation->setNumRequiredBookingsForSkillTriggering($this->assSettings->get(
1941            'ass_skl_trig_num_answ_barrier',
1942            ilObjAssessmentFolder::DEFAULT_SKL_TRIG_NUM_ANSWERS_BARRIER
1943        ));
1944
1945
1946        $skillEvaluation->init($questionList);
1947        $skillEvaluation->evaluate($testResults);
1948
1949        $skillEvaluation->handleSkillTriggering();
1950    }
1951
1952    abstract protected function buildTestPassQuestionList();
1953
1954    protected function showAnswerOptionalQuestionsConfirmation()
1955    {
1956        require_once 'Modules/Test/classes/confirmations/class.ilTestAnswerOptionalQuestionsConfirmationGUI.php';
1957        $confirmation = new ilTestAnswerOptionalQuestionsConfirmationGUI($this->lng);
1958
1959        $confirmation->setFormAction($this->ctrl->getFormAction($this));
1960        $confirmation->setCancelCmd('cancelAnswerOptionalQuestions');
1961        $confirmation->setConfirmCmd('confirmAnswerOptionalQuestions');
1962
1963        $confirmation->build($this->object->isFixedTest());
1964
1965        $this->populateHelperGuiContent($confirmation);
1966    }
1967
1968    protected function confirmAnswerOptionalQuestionsCmd()
1969    {
1970        $this->testSequence->setAnsweringOptionalQuestionsConfirmed(true);
1971        $this->testSequence->saveToDb();
1972
1973        $this->ctrl->setParameter($this, 'activecommand', 'gotoquestion');
1974        $this->ctrl->redirect($this, 'redirectQuestion');
1975    }
1976
1977    protected function cancelAnswerOptionalQuestionsCmd()
1978    {
1979        if ($this->object->getListOfQuestions()) {
1980            $this->ctrl->setParameter($this, 'activecommand', 'summary');
1981        } else {
1982            $this->ctrl->setParameter($this, 'activecommand', 'previous');
1983        }
1984
1985        $this->ctrl->redirect($this, 'redirectQuestion');
1986    }
1987
1988    /**
1989     * @param $helperGui
1990     */
1991    protected function populateHelperGuiContent($helperGui)
1992    {
1993        if ($this->object->getKioskMode()) {
1994            //$this->tpl->setBodyClass("kiosk");
1995            $this->tpl->hideFooter();
1996            $this->tpl->addBlockfile('CONTENT', 'adm_content', "tpl.il_as_tst_kiosk_mode_content.html", "Modules/Test");
1997            $this->tpl->setContent($this->ctrl->getHTML($helperGui));
1998        } else {
1999            $this->tpl->setVariable($this->getContentBlockName(), $this->ctrl->getHTML($helperGui));
2000        }
2001    }
2002
2003    /**
2004     * @return bool $charSelectorAvailable
2005     */
2006    protected function populateCharSelectorIfRequired()
2007    {
2008        global $DIC;
2009        $ilSetting = $DIC['ilSetting'];
2010
2011        if ($ilSetting->get('char_selector_availability') > 0) {
2012            require_once 'Services/UIComponent/CharSelector/classes/class.ilCharSelectorGUI.php';
2013            $char_selector = ilCharSelectorGUI::_getCurrentGUI($this->object);
2014            if ($char_selector->getConfig()->getAvailability() == ilCharSelectorConfig::ENABLED) {
2015                $char_selector->addToPage();
2016                $this->tpl->setCurrentBlock('char_selector');
2017                $this->tpl->setVariable("CHAR_SELECTOR_TEMPLATE", $char_selector->getSelectorHtml());
2018                $this->tpl->parseCurrentBlock();
2019
2020                return true;
2021            }
2022        }
2023
2024        return false;
2025    }
2026
2027    protected function getTestNavigationToolbarGUI()
2028    {
2029        global $DIC;
2030        $ilUser = $DIC['ilUser'];
2031
2032        require_once 'Modules/Test/classes/class.ilTestNavigationToolbarGUI.php';
2033        $navigationToolbarGUI = new ilTestNavigationToolbarGUI($this->ctrl, $this->lng, $this);
2034
2035        $navigationToolbarGUI->setSuspendTestButtonEnabled($this->object->getShowCancel());
2036        $navigationToolbarGUI->setQuestionTreeButtonEnabled($this->object->getListOfQuestions());
2037        $navigationToolbarGUI->setQuestionTreeVisible($ilUser->getPref('side_list_of_questions'));
2038        $navigationToolbarGUI->setQuestionListButtonEnabled($this->object->getListOfQuestions());
2039        $navigationToolbarGUI->setFinishTestCommand($this->getFinishTestCommand());
2040
2041        return $navigationToolbarGUI;
2042    }
2043
2044    protected function buildReadOnlyStateQuestionNavigationGUI($questionId)
2045    {
2046        require_once 'Modules/Test/classes/class.ilTestQuestionNavigationGUI.php';
2047        $navigationGUI = new ilTestQuestionNavigationGUI($this->lng);
2048
2049        if (!$this->isParticipantsAnswerFixed($questionId)) {
2050            $navigationGUI->setEditSolutionCommand(ilTestPlayerCommands::EDIT_SOLUTION);
2051        }
2052
2053        if ($this->object->getShowMarker()) {
2054            include_once "./Modules/Test/classes/class.ilObjTest.php";
2055            $solved_array = ilObjTest::_getSolvedQuestions($this->testSession->getActiveId(), $questionId);
2056            $solved = 0;
2057
2058            if (count($solved_array) > 0) {
2059                $solved = array_pop($solved_array);
2060                $solved = $solved["solved"];
2061            }
2062            // fau: testNav - change question mark command to link target
2063            if ($solved == 1) {
2064                $navigationGUI->setQuestionMarkLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::UNMARK_QUESTION));
2065                $navigationGUI->setQuestionMarked(true);
2066            } else {
2067                $navigationGUI->setQuestionMarkLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::MARK_QUESTION));
2068                $navigationGUI->setQuestionMarked(false);
2069            }
2070        }
2071        // fau.
2072
2073        return $navigationGUI;
2074    }
2075
2076    protected function buildEditableStateQuestionNavigationGUI($questionId, $charSelectorAvailable)
2077    {
2078        require_once 'Modules/Test/classes/class.ilTestQuestionNavigationGUI.php';
2079        $navigationGUI = new ilTestQuestionNavigationGUI($this->lng);
2080
2081        if ($this->object->isForceInstantFeedbackEnabled()) {
2082            $navigationGUI->setSubmitSolutionCommand(ilTestPlayerCommands::SUBMIT_SOLUTION);
2083        } else {
2084            // fau: testNav - use simple "submitSolution" button instead of "submitSolutionAndNext"
2085            $navigationGUI->setSubmitSolutionCommand(ilTestPlayerCommands::SUBMIT_SOLUTION);
2086            // fau.
2087        }
2088
2089        // fau: testNav - add a 'revert changes' link for editable question
2090        $navigationGUI->setRevertChangesLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::REVERT_CHANGES));
2091        // fau.
2092
2093
2094        // feedback
2095        switch (1) {
2096            case $this->object->getSpecificAnswerFeedback():
2097            case $this->object->getGenericAnswerFeedback():
2098            case $this->object->getAnswerFeedbackPoints():
2099            case $this->object->getInstantFeedbackSolution():
2100
2101                $navigationGUI->setAnswerFreezingEnabled($this->object->isInstantFeedbackAnswerFixationEnabled());
2102
2103                if ($this->object->isForceInstantFeedbackEnabled()) {
2104                    $navigationGUI->setForceInstantResponseEnabled(true);
2105                    $navigationGUI->setInstantFeedbackCommand(ilTestPlayerCommands::SUBMIT_SOLUTION);
2106                } else {
2107                    $navigationGUI->setInstantFeedbackCommand(ilTestPlayerCommands::SHOW_INSTANT_RESPONSE);
2108                }
2109        }
2110
2111        // hints
2112        if ($this->object->isOfferingQuestionHintsEnabled()) {
2113            $activeId = $this->testSession->getActiveId();
2114            $pass = $this->testSession->getPass();
2115
2116            require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionHintTracking.php';
2117            $questionHintTracking = new ilAssQuestionHintTracking($questionId, $activeId, $pass);
2118
2119            if ($questionHintTracking->requestsPossible()) {
2120                $navigationGUI->setRequestHintCommand(ilTestPlayerCommands::CONFIRM_HINT_REQUEST);
2121            }
2122
2123            if ($questionHintTracking->requestsExist()) {
2124                $navigationGUI->setShowHintsCommand(ilTestPlayerCommands::SHOW_REQUESTED_HINTS_LIST);
2125            }
2126        }
2127
2128        $navigationGUI->setCharSelectorEnabled($charSelectorAvailable);
2129
2130        if ($this->object->getShowMarker()) {
2131            include_once "./Modules/Test/classes/class.ilObjTest.php";
2132            $solved_array = ilObjTest::_getSolvedQuestions($this->testSession->getActiveId(), $questionId);
2133            $solved = 0;
2134
2135            if (count($solved_array) > 0) {
2136                $solved = array_pop($solved_array);
2137                $solved = $solved["solved"];
2138            }
2139
2140            // fau: testNav - change question mark command to link target
2141            if ($solved == 1) {
2142                $navigationGUI->setQuestionMarkLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::UNMARK_QUESTION_SAVE));
2143                $navigationGUI->setQuestionMarked(true);
2144            } else {
2145                $navigationGUI->setQuestionMarkLinkTarget($this->ctrl->getLinkTarget($this, ilTestPlayerCommands::MARK_QUESTION_SAVE));
2146                $navigationGUI->setQuestionMarked(false);
2147            }
2148        }
2149        // fau.
2150        return $navigationGUI;
2151    }
2152
2153    /**
2154     * @return string
2155     */
2156    protected function getFinishTestCommand()
2157    {
2158        if (!$this->object->getListOfQuestionsEnd()) {
2159            return 'finishTest';
2160        }
2161
2162        if ($this->object->areObligationsEnabled()) {
2163            $allObligationsAnswered = ilObjTest::allObligationsAnswered(
2164                $this->testSession->getTestId(),
2165                $this->testSession->getActiveId(),
2166                $this->testSession->getPass()
2167            );
2168
2169            if (!$allObligationsAnswered) {
2170                return 'outQuestionSummaryWithObligationsInfo';
2171            }
2172        }
2173
2174        return 'outQuestionSummary';
2175    }
2176
2177    // fau: testNav - populateIntermediateSolutionSaver is obsolete and can be deletd.
2178    //	/**
2179    //	 * @param assQuestionGUI $questionGui
2180    //	 */
2181    //	protected function populateIntermediateSolutionSaver(assQuestionGUI $questionGui)
2182    //	{
2183    //		$this->tpl->addJavaScript(ilUtil::getJSLocation("autosave.js", "Modules/Test"));
2184//
2185    //		$this->tpl->setVariable("AUTOSAVE_URL", $this->ctrl->getFormAction(
2186    //			$this, ilTestPlayerCommands::AUTO_SAVE, "", true
2187    //		));
2188//
2189    //		if( $questionGui->isAutosaveable() && $this->object->getAutosave() )
2190    //		{
2191    //			$formAction = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::AUTO_SAVE, '', false, false);
2192//
2193    //			$this->tpl->touchBlock('autosave');
2194    //			$this->tpl->setVariable("AUTOSAVEFORMACTION", $formAction);
2195    //			$this->tpl->setVariable("AUTOSAVEINTERVAL", $this->object->getAutosaveIval());
2196    //		}
2197    //	}
2198    // fau.
2199
2200    // fau: testNav - new function populateInstantResponseModal()
2201    protected function populateInstantResponseModal(assQuestionGUI $questionGui, $navUrl)
2202    {
2203        $questionGui->setNavigationGUI(null);
2204        $questionGui->getQuestionHeaderBlockBuilder()->setQuestionAnswered(true);
2205
2206        $answerFeedbackEnabled = $this->object->getSpecificAnswerFeedback();
2207
2208        $solutionoutput = $questionGui->getSolutionOutput(
2209            $this->testSession->getActiveId(), 	#active_id
2210            $this->testSession->getPass(),		#pass
2211            false, 								#graphical_output
2212            false,								#result_output
2213            true, 								#show_question_only
2214            $answerFeedbackEnabled,				#show_feedback
2215            false, 								#show_correct_solution
2216            false, 								#show_manual_scoring
2217            true								#show_question_text
2218        );
2219
2220        $pageoutput = $questionGui->outQuestionPage(
2221            "",
2222            $this->isShowingPostponeStatusReguired($questionGui->object->getId()),
2223            $this->testSession->getActiveId(),
2224            $solutionoutput
2225        );
2226
2227        $tpl = new ilTemplate('tpl.tst_player_response_modal.html', true, true, 'Modules/Test');
2228
2229        // populate the instant response blocks in the
2230        $saved_tpl = $this->tpl;
2231        $this->tpl = $tpl;
2232        $this->populateInstantResponseBlocks($questionGui, true);
2233        $this->tpl = $saved_tpl;
2234
2235        $tpl->setVariable('QUESTION_OUTPUT', $pageoutput);
2236
2237        $button = ilLinkButton::getInstance();
2238        $button->setId('tst_confirm_feedback');
2239        $button->setUrl($navUrl);
2240        $button->setCaption('proceed');
2241        $button->setPrimary(true);
2242        $tpl->setVariable('BUTTON', $button->render());
2243
2244
2245        require_once('Services/UIComponent/Modal/classes/class.ilModalGUI.php');
2246        $modal = ilModalGUI::getInstance();
2247        $modal->setType(ilModalGUI::TYPE_LARGE);
2248        $modal->setId('tst_question_feedback_modal');
2249        $modal->setHeading($this->lng->txt('tst_instant_feedback'));
2250        $modal->setBody($tpl->get());
2251
2252        $this->tpl->addOnLoadCode("$('#tst_question_feedback_modal').modal('show');");
2253        $this->tpl->setVariable('INSTANT_RESPONSE_MODAL', $modal->getHTML());
2254    }
2255    // fau;
2256
2257
2258    /**
2259     * @param assQuestionGUI $questionGui
2260     */
2261    protected function populateInstantResponseBlocks(assQuestionGUI $questionGui, $authorizedSolution)
2262    {
2263        $this->populateFeedbackBlockHeader(
2264            !$this->object->getSpecificAnswerFeedback() || !$questionGui->hasInlineFeedback()
2265        );
2266
2267        // This controls if the solution should be shown.
2268        // It gets the parameter "Scoring and Results" -> "Instant Feedback" -> "Show Solutions"
2269        if ($this->object->getInstantFeedbackSolution()) {
2270            $show_question_inline_score = $this->determineInlineScoreDisplay();
2271
2272            // Notation of the params prior to getting rid of this crap in favor of a class
2273            $solutionoutput = $questionGui->getSolutionOutput(
2274                $this->testSession->getActiveId(),    #active_id
2275                null,                                                #pass
2276                false,                                                #graphical_output
2277                $show_question_inline_score,                        #result_output
2278                true,                                                #show_question_only
2279                false,                                                #show_feedback
2280                true,                                                #show_correct_solution
2281                false,                                                #show_manual_scoring
2282                false                                                #show_question_text
2283            );
2284            $solutionoutput = str_replace('<h1 class="ilc_page_title_PageTitle"></h1>', '', $solutionoutput);
2285            $this->populateSolutionBlock($solutionoutput);
2286        }
2287
2288        $reachedPoints = $questionGui->object->getAdjustedReachedPoints(
2289            $this->testSession->getActiveId(),
2290            null,
2291            $authorizedSolution
2292        );
2293
2294        $maxPoints = $questionGui->object->getMaximumPoints();
2295
2296        $solutionCorrect = ($reachedPoints == $maxPoints);
2297
2298        // This controls if the score should be shown.
2299        // It gets the parameter "Scoring and Results" -> "Instant Feedback" -> "Show Results (Only Points)"
2300        if ($this->object->getAnswerFeedbackPoints()) {
2301            $this->populateScoreBlock($reachedPoints, $maxPoints);
2302        }
2303
2304        // This controls if the generic feedback should be shown.
2305        // It gets the parameter "Scoring and Results" -> "Instant Feedback" -> "Show Solutions"
2306        if ($this->object->getGenericAnswerFeedback()) {
2307            $this->populateGenericFeedbackBlock($questionGui, $solutionCorrect);
2308        }
2309
2310        // This controls if the specific feedback should be shown.
2311        // It gets the parameter "Scoring and Results" -> "Instant Feedback" -> "Show Answer-Specific Feedback"
2312        if ($this->object->getSpecificAnswerFeedback()) {
2313            $this->populateSpecificFeedbackBlock($questionGui);
2314        }
2315    }
2316
2317    protected function populateFeedbackBlockHeader($withFocusAnchor)
2318    {
2319        if ($withFocusAnchor) {
2320            $this->tpl->setCurrentBlock('inst_resp_id');
2321            $this->tpl->setVariable('INSTANT_RESPONSE_FOCUS_ID', 'focus');
2322            $this->tpl->parseCurrentBlock();
2323        }
2324
2325        $this->tpl->setCurrentBlock('instant_response_header');
2326        $this->tpl->setVariable('INSTANT_RESPONSE_HEADER', $this->lng->txt('tst_feedback'));
2327        $this->tpl->parseCurrentBlock();
2328    }
2329
2330    protected function getCurrentSequenceElement()
2331    {
2332        if ($this->getSequenceElementParameter()) {
2333            return $this->getSequenceElementParameter();
2334        }
2335
2336        return $this->testSession->getLastSequence();
2337    }
2338
2339    protected function resetSequenceElementParameter()
2340    {
2341        unset($_GET['sequence']);
2342        $this->ctrl->setParameter($this, 'sequence', null);
2343    }
2344
2345    protected function getSequenceElementParameter()
2346    {
2347        if (isset($_GET['sequence'])) {
2348            return $_GET['sequence'];
2349        }
2350
2351        return null;
2352    }
2353
2354    protected function getPresentationModeParameter()
2355    {
2356        if (isset($_GET['pmode'])) {
2357            return $_GET['pmode'];
2358        }
2359
2360        return null;
2361    }
2362
2363    protected function getInstantResponseParameter()
2364    {
2365        if (isset($_GET['instresp'])) {
2366            return $_GET['instresp'];
2367        }
2368
2369        return null;
2370    }
2371
2372    protected function getNextCommandParameter()
2373    {
2374        if (isset($_POST['nextcmd']) && strlen($_POST['nextcmd'])) {
2375            return $_POST['nextcmd'];
2376        }
2377
2378        return null;
2379    }
2380
2381    protected function getNextSequenceParameter()
2382    {
2383        if (isset($_POST['nextseq']) && is_numeric($_POST['nextseq'])) {
2384            return (int) $_POST['nextseq'];
2385        }
2386
2387        return null;
2388    }
2389
2390    // fau: testNav - get the navigation url set by a submit from ilTestPlayerNavigationControl.js
2391    protected function getNavigationUrlParameter()
2392    {
2393        if (isset($_POST['test_player_navigation_url'])) {
2394            return $_POST['test_player_navigation_url'];
2395        }
2396        return null;
2397    }
2398    // fau.
2399
2400    // fau: testNav - get set and check the 'answer_changed' url parameter
2401    /**
2402     * Get the 'answer changed' status from the current request
2403     * It may be set by ilTestPlayerNavigationControl.js or by a previousRequest
2404     * @return bool
2405     */
2406    protected function getAnswerChangedParameter()
2407    {
2408        return !empty($_GET['test_answer_changed']);
2409    }
2410
2411    /**
2412     * Set the 'answer changed' url parameter for generated links
2413     * @param bool $changed
2414     */
2415    protected function setAnswerChangedParameter($changed = true)
2416    {
2417        $this->ctrl->setParameter($this, 'test_answer_changed', $changed ? '1' : '0');
2418    }
2419
2420
2421    /**
2422     * Check the 'answer changed' parameter when a question form is intermediately submitted
2423     * - save or delete the intermediate solution
2424     * - save the parameter for the next request
2425     */
2426    protected function handleIntermediateSubmit()
2427    {
2428        if ($this->getAnswerChangedParameter()) {
2429            $this->saveQuestionSolution(false);
2430        } else {
2431            $this->removeIntermediateSolution();
2432        }
2433        $this->setAnswerChangedParameter($this->getAnswerChangedParameter());
2434    }
2435    // fau.
2436
2437    // fau: testNav - save the switch to prevent the navigation confirmation
2438    /**
2439     * Save the save the switch to prevent the navigation confirmation
2440     */
2441    protected function saveNavigationPreventConfirmation()
2442    {
2443        if (!empty($_POST['save_on_navigation_prevent_confirmation'])) {
2444            $_SESSION['save_on_navigation_prevent_confirmation'] = true;
2445        }
2446
2447        if (!empty($_POST[self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM])) {
2448            $_SESSION[self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM] = true;
2449        }
2450    }
2451    // fau.
2452
2453    /**
2454     * @var array[assQuestion]
2455     */
2456    private $cachedQuestionGuis = array();
2457
2458    /**
2459     * @param $questionId
2460     * @param $sequenceElement
2461     * @return object
2462     */
2463    protected function getQuestionGuiInstance($questionId, $fromCache = true)
2464    {
2465        global $DIC;
2466        $tpl = $DIC['tpl'];
2467
2468        if (!$fromCache || !isset($this->cachedQuestionGuis[$questionId])) {
2469            $questionGui = $this->object->createQuestionGUI("", $questionId);
2470            $questionGui->setTargetGui($this);
2471            $questionGui->setPresentationContext(assQuestionGUI::PRESENTATION_CONTEXT_TEST);
2472            $questionGui->object->setObligationsToBeConsidered($this->object->areObligationsEnabled());
2473            $questionGui->populateJavascriptFilesRequiredForWorkForm($tpl);
2474            $questionGui->object->setOutputType(OUTPUT_JAVASCRIPT);
2475            $questionGui->object->setShuffler($this->buildQuestionAnswerShuffler($questionId));
2476
2477            // hey: prevPassSolutions - determine solution pass index and configure gui accordingly
2478            $this->initTestQuestionConfig($questionGui->object);
2479            // hey.
2480
2481            $this->cachedQuestionGuis[$questionId] = $questionGui;
2482        }
2483
2484        return $this->cachedQuestionGuis[$questionId];
2485    }
2486
2487    /**
2488     * @var array[assQuestion]
2489     */
2490    private $cachedQuestionObjects = array();
2491
2492    /**
2493     * @param $questionId
2494     * @return assQuestion
2495     */
2496    protected function getQuestionInstance($questionId, $fromCache = true)
2497    {
2498        global $DIC;
2499        $ilDB = $DIC['ilDB'];
2500        $ilUser = $DIC['ilUser'];
2501
2502        if (!$fromCache || !isset($this->cachedQuestionObjects[$questionId])) {
2503            $questionOBJ = assQuestion::_instantiateQuestion($questionId);
2504
2505            $assSettings = new ilSetting('assessment');
2506            require_once 'Modules/TestQuestionPool/classes/class.ilAssQuestionProcessLockerFactory.php';
2507            $processLockerFactory = new ilAssQuestionProcessLockerFactory($assSettings, $ilDB);
2508            $processLockerFactory->setQuestionId($questionOBJ->getId());
2509            $processLockerFactory->setUserId($ilUser->getId());
2510            include_once("./Modules/Test/classes/class.ilObjAssessmentFolder.php");
2511            $processLockerFactory->setAssessmentLogEnabled(ilObjAssessmentFolder::_enabledAssessmentLogging());
2512            $questionOBJ->setProcessLocker($processLockerFactory->getLocker());
2513
2514            $questionOBJ->setObligationsToBeConsidered($this->object->areObligationsEnabled());
2515            $questionOBJ->setOutputType(OUTPUT_JAVASCRIPT);
2516
2517            // hey: prevPassSolutions - determine solution pass index and configure gui accordingly
2518            $this->initTestQuestionConfig($questionOBJ);
2519            // hey.
2520
2521            $this->cachedQuestionObjects[$questionId] = $questionOBJ;
2522        }
2523
2524        return $this->cachedQuestionObjects[$questionId];
2525    }
2526
2527    // hey: prevPassSolutions - determine solution pass index and configure gui accordingly
2528    protected function initTestQuestionConfig(assQuestion $questionOBJ)
2529    {
2530        $questionOBJ->getTestPresentationConfig()->setPreviousPassSolutionReuseAllowed(
2531            $this->object->isPreviousSolutionReuseEnabled($this->testSession->getActiveId())
2532        );
2533    }
2534    // hey.
2535
2536    /**
2537     * @param $questionId
2538     * @return ilArrayElementShuffler
2539     */
2540    protected function buildQuestionAnswerShuffler($questionId)
2541    {
2542        require_once 'Services/Randomization/classes/class.ilArrayElementShuffler.php';
2543        $shuffler = new ilArrayElementShuffler();
2544
2545        $fixedSeed = $this->buildFixedShufflerSeed($questionId);
2546        $shuffler->setSeed($fixedSeed);
2547
2548        return $shuffler;
2549    }
2550
2551    /**
2552     * @param $sequence
2553     * @param $questionId
2554     * @param $ilLog
2555     */
2556    protected function handleTearsAndAngerQuestionIsNull($questionId, $sequenceElement)
2557    {
2558        global $DIC;
2559        $ilLog = $DIC['ilLog'];
2560
2561        $ilLog->write(
2562            "INV SEQ:"
2563            . "active={$this->testSession->getActiveId()} "
2564            . "qId=$questionId seq=$sequenceElement "
2565            . serialize($this->testSequence)
2566        );
2567
2568        $ilLog->logStack('INV SEQ');
2569
2570        $this->ctrl->setParameter($this, 'sequence', $this->testSequence->getFirstSequence());
2571        $this->ctrl->redirect($this, ilTestPlayerCommands::SHOW_QUESTION);
2572    }
2573
2574    /**
2575     * @param $contentHTML
2576     */
2577    protected function populateMessageContent($contentHTML)
2578    {
2579        if ($this->object->getKioskMode()) {
2580            $this->tpl->addBlockfile($this->getContentBlockName(), 'content', "tpl.il_as_tst_kiosk_mode_content.html", "Modules/Test");
2581            $this->tpl->setContent($contentHTML);
2582        } else {
2583            $this->tpl->setVariable($this->getContentBlockName(), $contentHTML);
2584        }
2585    }
2586
2587    protected function populateModals()
2588    {
2589        require_once 'Services/UIComponent/Button/classes/class.ilSubmitButton.php';
2590        require_once 'Services/UIComponent/Button/classes/class.ilLinkButton.php';
2591        require_once 'Services/UIComponent/Modal/classes/class.ilModalGUI.php';
2592
2593        $this->populateDiscardSolutionModal();
2594        // fau: testNav - populateNavWhenChangedModal instead of populateNavWhileEditModal
2595        $this->populateNavWhenChangedModal();
2596        // fau.
2597
2598        if ($this->object->isFollowupQuestionAnswerFixationEnabled()) {
2599            $this->populateNextLocksChangedModal();
2600
2601            $this->populateNextLocksUnchangedModal();
2602        }
2603    }
2604
2605    protected function populateDiscardSolutionModal()
2606    {
2607        $tpl = new ilTemplate('tpl.tst_player_confirmation_modal.html', true, true, 'Modules/Test');
2608
2609        $tpl->setVariable('CONFIRMATION_TEXT', $this->lng->txt('discard_answer_confirmation'));
2610
2611        $button = ilSubmitButton::getInstance();
2612        $button->setCommand(ilTestPlayerCommands::DISCARD_SOLUTION);
2613        $button->setCaption('discard_answer');
2614        $tpl->setCurrentBlock('buttons');
2615        $tpl->setVariable('BUTTON', $button->render());
2616        $tpl->parseCurrentBlock();
2617
2618        $button = ilLinkButton::getInstance();
2619        $button->setId('tst_cancel_discard_button');
2620        $button->setCaption('cancel');
2621        $button->setPrimary(true);
2622        $tpl->setCurrentBlock('buttons');
2623        $tpl->setVariable('BUTTON', $button->render());
2624        $tpl->parseCurrentBlock();
2625
2626        $modal = ilModalGUI::getInstance();
2627        $modal->setId('tst_discard_solution_modal');
2628        $modal->setHeading($this->lng->txt('discard_answer'));
2629        $modal->setBody($tpl->get());
2630
2631        $this->tpl->setCurrentBlock('discard_solution_modal');
2632        $this->tpl->setVariable('DISCARD_SOLUTION_MODAL', $modal->getHTML());
2633        $this->tpl->parseCurrentBlock();
2634
2635        // fau: testNav - the discard solution modal is now handled by ilTestPlayerNavigationControl.js
2636//		$this->tpl->addJavaScript('Modules/Test/js/ilTestPlayerDiscardSolutionModal.js', true);
2637// fau.
2638    }
2639
2640    // fau: testNav - populateNavWhileEditModal is obsolete and can be deleted.
2641    //	protected function populateNavWhileEditModal()
2642    //	{
2643    //		require_once 'Services/Form/classes/class.ilFormPropertyGUI.php';
2644    //		require_once 'Services/Form/classes/class.ilHiddenInputGUI.php';
2645//
2646    //		$tpl = new ilTemplate('tpl.tst_player_confirmation_modal.html', true, true, 'Modules/Test');
2647//
2648    //		$tpl->setVariable('CONFIRMATION_TEXT', $this->lng->txt('tst_nav_while_edit_modal_text'));
2649//
2650    //		$button = ilSubmitButton::getInstance();
2651    //		$button->setCommand(ilTestPlayerCommands::SUBMIT_SOLUTION);
2652    //		$button->setCaption('tst_nav_while_edit_modal_save_btn');
2653    //		$button->setPrimary(true);
2654    //		$tpl->setCurrentBlock('buttons');
2655    //		$tpl->setVariable('BUTTON', $button->render());
2656    //		$tpl->parseCurrentBlock();
2657//
2658    //		foreach(array('nextcmd', 'nextseq') as $hiddenPostVar)
2659    //		{
2660    //			$nextCmdInp = new ilHiddenInputGUI($hiddenPostVar);
2661    //			$nextCmdInp->setValue('');
2662    //			$tpl->setCurrentBlock('hidden_inputs');
2663    //			$tpl->setVariable('HIDDEN_INPUT', $nextCmdInp->getToolbarHTML());
2664    //			$tpl->parseCurrentBlock();
2665    //		}
2666//
2667    //		$button = ilLinkButton::getInstance();
2668    //		$this->ctrl->setParameter($this, 'pmode', self::PRESENTATION_MODE_VIEW);
2669    //		$button->setId('nextCmdLink');
2670    //		$button->setUrl('#');
2671    //		$this->ctrl->setParameter($this, 'pmode', $this->getPresentationModeParameter());
2672    //		$button->setCaption('tst_nav_while_edit_modal_nosave_btn');
2673    //		$tpl->setCurrentBlock('buttons');
2674    //		$tpl->setVariable('BUTTON', $button->render());
2675    //		$tpl->parseCurrentBlock();
2676//
2677    //		$button = ilLinkButton::getInstance();
2678    //		$button->setId('tst_cancel_nav_while_edit_button');
2679    //		$button->setCaption('tst_nav_while_edit_modal_cancel_btn');
2680    //		$tpl->setCurrentBlock('buttons');
2681    //		$tpl->setVariable('BUTTON', $button->render());
2682    //		$tpl->parseCurrentBlock();
2683//
2684    //		$modal = ilModalGUI::getInstance();
2685    //		$modal->setId('tst_nav_while_edit_modal');
2686    //		$modal->setHeading($this->lng->txt('tst_nav_while_edit_modal_header'));
2687    //		$modal->setBody($tpl->get());
2688//
2689    //		$this->tpl->setCurrentBlock('nav_while_edit_modal');
2690    //		$this->tpl->setVariable('NAV_WHILE_EDIT_MODAL', $modal->getHTML());
2691    //		$this->tpl->parseCurrentBlock();
2692//
2693    //		$this->tpl->addJavaScript('Modules/Test/js/ilTestPlayerNavWhileEditModal.js', true);
2694    //	}
2695    // fau.
2696
2697    // fau: testNav - new function populateNavWhenChangedModal
2698    protected function populateNavWhenChangedModal()
2699    {
2700        return; // usibility fix: get rid of popup
2701
2702        if (!empty($_SESSION['save_on_navigation_prevent_confirmation'])) {
2703            return;
2704        }
2705
2706        $tpl = new ilTemplate('tpl.tst_player_confirmation_modal.html', true, true, 'Modules/Test');
2707
2708        if ($this->object->isInstantFeedbackAnswerFixationEnabled() && $this->object->isForceInstantFeedbackEnabled()) {
2709            $text = $this->lng->txt('save_on_navigation_locked_confirmation');
2710        } else {
2711            $text = $this->lng->txt('save_on_navigation_confirmation');
2712        }
2713        if ($this->object->isForceInstantFeedbackEnabled()) {
2714            $text .= " " . $this->lng->txt('save_on_navigation_forced_feedback_hint');
2715        }
2716        $tpl->setVariable('CONFIRMATION_TEXT', $text);
2717
2718
2719        $button = ilLinkButton::getInstance();
2720        $button->setId('tst_save_on_navigation_button');
2721        $button->setUrl('#');
2722        $button->setCaption('tst_save_and_proceed');
2723        $button->setPrimary(true);
2724        $tpl->setCurrentBlock('buttons');
2725        $tpl->setVariable('BUTTON', $button->render());
2726        $tpl->parseCurrentBlock();
2727
2728        $button = ilLinkButton::getInstance();
2729        $button->setId('tst_cancel_on_navigation_button');
2730        $button->setUrl('#');
2731        $button->setCaption('cancel');
2732        $button->setPrimary(false);
2733        $tpl->setCurrentBlock('buttons');
2734        $tpl->setVariable('BUTTON', $button->render());
2735        $tpl->parseCurrentBlock();
2736
2737        $tpl->setCurrentBlock('checkbox');
2738        $tpl->setVariable('CONFIRMATION_CHECKBOX_NAME', 'save_on_navigation_prevent_confirmation');
2739        $tpl->setVariable('CONFIRMATION_CHECKBOX_LABEL', $this->lng->txt('tst_dont_show_msg_again_in_current_session'));
2740        $tpl->parseCurrentBlock();
2741
2742        $modal = ilModalGUI::getInstance();
2743        $modal->setId('tst_save_on_navigation_modal');
2744        $modal->setHeading($this->lng->txt('save_on_navigation'));
2745        $modal->setBody($tpl->get());
2746
2747        $this->tpl->setCurrentBlock('nav_while_edit_modal');
2748        $this->tpl->setVariable('NAV_WHILE_EDIT_MODAL', $modal->getHTML());
2749        $this->tpl->parseCurrentBlock();
2750    }
2751    // fau.
2752
2753    protected function populateNextLocksUnchangedModal()
2754    {
2755        require_once 'Modules/Test/classes/class.ilTestPlayerConfirmationModal.php';
2756        $modal = new ilTestPlayerConfirmationModal();
2757        $modal->setModalId('tst_next_locks_unchanged_modal');
2758
2759        $modal->setHeaderText($this->lng->txt('tst_nav_next_locks_empty_answer_header'));
2760        $modal->setConfirmationText($this->lng->txt('tst_nav_next_locks_empty_answer_confirm'));
2761
2762        $button = $modal->buildModalButtonInstance('tst_nav_next_empty_answer_button');
2763        $button->setCaption('tst_proceed');
2764        $button->setPrimary(false);
2765        $modal->addButton($button);
2766
2767        $button = $modal->buildModalButtonInstance('tst_cancel_next_empty_answer_button');
2768        $button->setCaption('cancel');
2769        $button->setPrimary(true);
2770        $modal->addButton($button);
2771
2772        $this->tpl->setCurrentBlock('next_locks_unchanged_modal');
2773        $this->tpl->setVariable('NEXT_LOCKS_UNCHANGED_MODAL', $modal->getHTML());
2774        $this->tpl->parseCurrentBlock();
2775    }
2776
2777    protected function populateNextLocksChangedModal()
2778    {
2779        if ($this->isFollowUpQuestionLocksConfirmationPrevented()) {
2780            return;
2781        }
2782
2783        require_once 'Modules/Test/classes/class.ilTestPlayerConfirmationModal.php';
2784        $modal = new ilTestPlayerConfirmationModal();
2785        $modal->setModalId('tst_next_locks_changed_modal');
2786
2787        $modal->setHeaderText($this->lng->txt('tst_nav_next_locks_current_answer_header'));
2788        $modal->setConfirmationText($this->lng->txt('tst_nav_next_locks_current_answer_confirm'));
2789
2790        $modal->setConfirmationCheckboxName(self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM);
2791        $modal->setConfirmationCheckboxLabel($this->lng->txt('tst_dont_show_msg_again_in_current_session'));
2792
2793        $button = $modal->buildModalButtonInstance('tst_nav_next_changed_answer_button');
2794        $button->setCaption('tst_save_and_proceed');
2795        $button->setPrimary(true);
2796        $modal->addButton($button);
2797
2798        $button = $modal->buildModalButtonInstance('tst_cancel_next_changed_answer_button');
2799        $button->setCaption('cancel');
2800        $button->setPrimary(false);
2801        $modal->addButton($button);
2802
2803        $this->tpl->setCurrentBlock('next_locks_changed_modal');
2804        $this->tpl->setVariable('NEXT_LOCKS_CHANGED_MODAL', $modal->getHTML());
2805        $this->tpl->parseCurrentBlock();
2806    }
2807
2808    const FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM = 'followup_qst_locks_prevent_confirmation';
2809
2810    protected function setFollowUpQuestionLocksConfirmationPrevented()
2811    {
2812        $_SESSION[self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM] = true;
2813    }
2814
2815    protected function isFollowUpQuestionLocksConfirmationPrevented()
2816    {
2817        if (!isset($_SESSION[self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM])) {
2818            return false;
2819        }
2820
2821        return $_SESSION[self::FOLLOWUP_QST_LOCKS_PREVENT_CONFIRMATION_PARAM];
2822    }
2823
2824    // fau: testNav - new function populateQuestionEditControl
2825    /**
2826     * Populate the navigation and saving control for editable questions
2827     *
2828     * @param assQuestionGUI 	$questionGUI
2829     */
2830    protected function populateQuestionEditControl($questionGUI)
2831    {
2832        // configuration for ilTestPlayerQuestionEditControl.js
2833        $config = array();
2834
2835        // set the initial state of the question
2836        $state = $questionGUI->object->lookupForExistingSolutions($this->testSession->getActiveId(), $this->testSession->getPass());
2837        $config['isAnswered'] = $state['authorized'];
2838        $config['isAnswerChanged'] = $state['intermediate'] || $this->getAnswerChangedParameter();
2839
2840        // set  url to which the for should be submitted when the working time is over
2841        // don't use asynch url because the form is submitted directly
2842        // but use simple '&' because url is copied by javascript into the form action
2843        $config['saveOnTimeReachedUrl'] = str_replace('&amp;', '&', $this->ctrl->getFormAction($this, ilTestPlayerCommands::AUTO_SAVE_ON_TIME_LIMIT));
2844
2845        // enable the auto saving function
2846        // the autosave url is asynch because it will be used by an ajax request
2847        if ($questionGUI->isAutosaveable() && $this->object->getAutosave()) {
2848            $config['autosaveUrl'] = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::AUTO_SAVE, '', true);
2849            $config['autosaveInterval'] = $this->object->getAutosaveIval();
2850        } else {
2851            $config['autosaveUrl'] = '';
2852            $config['autosaveInterval'] = 0;
2853        }
2854
2855        /** @var  ilTestQuestionConfig $questionConfig */
2856        // hey: prevPassSolutions - refactored method identifiers
2857        $questionConfig = $questionGUI->object->getTestPresentationConfig();
2858        // hey.
2859
2860        // Normal questions: changes are done in form fields an can be detected there
2861        $config['withFormChangeDetection'] = $questionConfig->isFormChangeDetectionEnabled();
2862
2863        // Flash and Java questions: changes are directly sent to ilias and have to be polled from there
2864        $config['withBackgroundChangeDetection'] = $questionConfig->isBackgroundChangeDetectionEnabled();
2865        $config['backgroundDetectorUrl'] = $this->ctrl->getLinkTarget($this, ilTestPlayerCommands::DETECT_CHANGES, '', true);
2866
2867        // Forced feedback will change the navigation saving command
2868        $config['forcedInstantFeedback'] = $this->object->isForceInstantFeedbackEnabled();
2869        $config['nextQuestionLocks'] = $this->object->isFollowupQuestionAnswerFixationEnabled();
2870
2871        $this->tpl->addJavascript('./Modules/Test/js/ilTestPlayerQuestionEditControl.js');
2872        $this->tpl->addOnLoadCode('il.TestPlayerQuestionEditControl.init(' . json_encode($config) . ')');
2873    }
2874    // fau.
2875
2876    protected function getQuestionsDefaultPresentationMode($isQuestionWorkedThrough)
2877    {
2878        // fau: testNav - always set default presentation mode to "edit"
2879        return self::PRESENTATION_MODE_EDIT;
2880        // fau.
2881    }
2882
2883    /**
2884     * @param $questionId
2885     * @return string
2886     */
2887    protected function buildFixedShufflerSeed($questionId)
2888    {
2889        $fixedSeed = $questionId . $this->testSession->getActiveId() . $this->testSession->getPass();
2890
2891        if (strlen($fixedSeed) < self::FIXED_SHUFFLER_SEED_MIN_LENGTH) {
2892            $fixedSeed *= (
2893                10 * (self::FIXED_SHUFFLER_SEED_MIN_LENGTH - strlen($fixedSeed))
2894            );
2895        }
2896
2897        return $fixedSeed;
2898    }
2899
2900    protected function registerForcedFeedbackNavUrl($forcedFeedbackNavUrl)
2901    {
2902        if (!isset($_SESSION['forced_feedback_navigation_url'])) {
2903            $_SESSION['forced_feedback_navigation_url'] = array();
2904        }
2905
2906        $_SESSION['forced_feedback_navigation_url'][$this->testSession->getActiveId()] = $forcedFeedbackNavUrl;
2907    }
2908
2909    protected function getRegisteredForcedFeedbackNavUrl()
2910    {
2911        if (!isset($_SESSION['forced_feedback_navigation_url'])) {
2912            return null;
2913        }
2914
2915        if (!isset($_SESSION['forced_feedback_navigation_url'][$this->testSession->getActiveId()])) {
2916            return null;
2917        }
2918
2919        return $_SESSION['forced_feedback_navigation_url'][$this->testSession->getActiveId()];
2920    }
2921
2922    protected function isForcedFeedbackNavUrlRegistered()
2923    {
2924        return !empty($this->getRegisteredForcedFeedbackNavUrl());
2925    }
2926
2927    protected function unregisterForcedFeedbackNavUrl()
2928    {
2929        if (isset($_SESSION['forced_feedback_navigation_url'][$this->testSession->getActiveId()])) {
2930            unset($_SESSION['forced_feedback_navigation_url'][$this->testSession->getActiveId()]);
2931        }
2932    }
2933}
2934