<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 */
/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner;

use oat\libCat\CatSession;
use oat\libCat\Exception\CatEngineException;
use oat\libCat\result\AbstractResult;
use oat\libCat\result\ItemResult;
use oat\taoDelivery\model\execution\DeliveryServerService;
use oat\taoQtiTest\helpers\TestSessionMemento;
use oat\taoQtiTest\models\event\QtiTestChangeEvent;
use oat\taoQtiTest\models\QtiTestCompilerIndex;
use oat\taoQtiTest\models\runner\session\TestSession;
use oat\taoQtiTest\models\SessionStateService;
use oat\taoQtiTest\models\cat\CatService;
use oat\taoQtiTest\models\ExtendedStateService;
use oat\taoQtiTest\models\SectionPauseService;
use oat\tao\helpers\UserHelper;
use qtism\data\AssessmentTest;
use qtism\data\AssessmentItemRef;
use qtism\data\NavigationMode;
use qtism\runtime\storage\binary\AbstractQtiBinaryStorage;
use qtism\runtime\storage\binary\BinaryAssessmentTestSeeker;
use qtism\runtime\tests\RouteItem;
use oat\oatbox\event\EventManager;
use oat\taoQtiTest\models\event\SelectAdaptiveNextItemEvent;
use oat\libCat\result\ResultVariable;
use taoQtiTest_models_classes_QtiTestService;

/**
 * Class QtiRunnerServiceContext
 *
 * Defines a container to store and to share runner service context of the QTI implementation
 *
 * @package oat\taoQtiTest\models
 */
class QtiRunnerServiceContext extends RunnerServiceContext
{
    /**
     * The session storage
     * @var AbstractQtiBinaryStorage
     */
    protected $storage;

    /**
     * @var \taoQtiTest_helpers_SessionManager
     */
    protected $sessionManager;

    /**
     * The assessment test definition
     * @var AssessmentTest
     */
    protected $testDefinition;

    /**
     * The path of the compilation directory.
     *
     * @var \tao_models_classes_service_StorageDirectory[]
     */
    protected $compilationDirectory;

    /**
     * The meta data about the test definition being executed.
     *
     * @var array
     */
    private $testMeta;

    /**
     * The index of compiled items.
     *
     * @var QtiTestCompilerIndex
     */
    private $itemIndex;

    /**
     * The URI of the assessment test
     * @var string
     */
    protected $testDefinitionUri;

    /**
     * The URI of the compiled delivery
     * @var string
     */
    protected $testCompilationUri;

    /**
     * The URI of the delivery execution
     * @var string
     */
    protected $testExecutionUri;

    /**
     * Whether we are in synchronization mode
     * @var boolean
     */
    private $syncingMode = false;

    /**
     * @var string
     */
    private $userUri;

    /**
     * QtiRunnerServiceContext constructor.
     *
     * @param string $testDefinitionUri
     * @param string $testCompilationUri
     * @param string $testExecutionUri
     */
    public function __construct($testDefinitionUri, $testCompilationUri, $testExecutionUri)
    {
        $this->testDefinitionUri = $testDefinitionUri;
        $this->testCompilationUri = $testCompilationUri;
        $this->testExecutionUri = $testExecutionUri;
    }

    /**
     * Starts the context
     */
    public function init()
    {
        // code borrowed from the previous implementation, maybe obsolete...
        /** @var SessionStateService $sessionStateService */
        $sessionStateService = $this->getServiceManager()->get(SessionStateService::SERVICE_ID);
        $sessionStateService->resumeSession($this->getTestSession());

        $this->retrieveItemIndex();
    }

    /**
     * Extracts the path of the compilation directory
     */
    protected function initCompilationDirectory()
    {
        $fileStorage = \tao_models_classes_service_FileStorage::singleton();
        $directoryIds = explode('|', $this->getTestCompilationUri());
        $directories = array(
            'private' => $fileStorage->getDirectoryById($directoryIds[0]),
            'public' => $fileStorage->getDirectoryById($directoryIds[1])
        );

        $this->compilationDirectory = $directories;
    }

    /**
     * Loads the test definition
     */
    protected function initTestDefinition()
    {
        $this->testDefinition = \taoQtiTest_helpers_Utils::getTestDefinition($this->getTestCompilationUri());
    }

    /**
     * Loads the storage
     * @throws \common_exception_Error
     * @throws \common_ext_ExtensionException
     */
    protected function initStorage()
    {
        /** @var DeliveryServerService $deliveryServerService */
        $deliveryServerService = $this->getServiceManager()->get(DeliveryServerService::SERVICE_ID);
        $resultStore = $deliveryServerService->getResultStoreWrapper($this->getTestExecutionUri());
        $testResource = new \core_kernel_classes_Resource($this->getTestDefinitionUri());
        $sessionManager = new \taoQtiTest_helpers_SessionManager($resultStore, $testResource);

        $seeker = new BinaryAssessmentTestSeeker($this->getTestDefinition());
        $userUri = $this->getUserUri();

        $config = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');
        $storageClassName = $config['test-session-storage'];
        $this->storage = new $storageClassName($sessionManager, $seeker, $userUri);
        $this->sessionManager = $sessionManager;
    }

    /**
     * Loads the test session
     * @throws \common_exception_Error
     */
    protected function initTestSession()
    {
        $storage = $this->getStorage();
        $sessionId = $this->getTestExecutionUri();

        if ($storage->exists($sessionId) === false) {
            \common_Logger::d("Instantiating QTI Assessment Test Session");
            $this->setTestSession($storage->instantiate($this->getTestDefinition(), $sessionId));

            $testTaker = $this->getTestTakerFromSessionOrRds();
            \taoQtiTest_helpers_TestRunnerUtils::setInitialOutcomes($this->getTestSession(), $testTaker);
        }
        else {
            \common_Logger::d("Retrieving QTI Assessment Test Session '${sessionId}'...");
            $this->setTestSession($storage->retrieve($this->getTestDefinition(), $sessionId));
        }

        \taoQtiTest_helpers_TestRunnerUtils::preserveOutcomes($this->getTestSession());
    }

    /**
     * @deprecated
     */
    protected function retrieveTestMeta()
    {
    }

    /**
     * Retrieves the index of compiled items.
     */
    protected function retrieveItemIndex()
    {
        $this->itemIndex = new QtiTestCompilerIndex();
        try {
            $directories = $this->getCompilationDirectory();
            $data = $directories['private']->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_INDEX);
            if ($data) {
                $this->itemIndex->unserialize($data);
            }
        } catch(\Exception $e) {
            \common_Logger::d('Ignoring file not found exception for Items Index');
        }
    }

    /**
     * Sets the test session
     * @param mixed $testSession
     * @throws \common_exception_InvalidArgumentType
     */
    public function setTestSession($testSession)
    {
        if ($testSession instanceof TestSession) {
            parent::setTestSession($testSession);
        } else {
            throw new \common_exception_InvalidArgumentType(
                'QtiRunnerServiceContext',
                'setTestSession',
                0,
                'oat\taoQtiTest\models\runner\session\TestSession',
                $testSession
            );
        }
    }

    /**
     * Gets the session storage
     * @return AbstractQtiBinaryStorage
     * @throws \common_exception_Error
     * @throws \common_ext_ExtensionException
     */
    public function getStorage()
    {
        if (!$this->storage) {
            $this->initStorage();
        }
        return $this->storage;
    }

    /**
     * @return EventManager
     * @throws \Zend\ServiceManager\Exception\ServiceNotFoundException
     */
    protected function getEventManager() {
        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
    }

    /**
     * @return \taoQtiTest_helpers_SessionManager
     * @throws \common_exception_Error
     * @throws \common_ext_ExtensionException
     */
    public function getSessionManager()
    {
        if (null === $this->sessionManager){
            $this->initStorage();
        }
        return $this->sessionManager;
    }

    /**
     * Gets the assessment test definition
     * @return AssessmentTest
     */
    public function getTestDefinition()
    {
        if (null === $this->testDefinition) {
            $this->initTestDefinition();
        }
        return $this->testDefinition;
    }

    /**
     * Gets the path of the compilation directory
     * @return \tao_models_classes_service_StorageDirectory[]
     */
    public function getCompilationDirectory()
    {
        if (null === $this->compilationDirectory) {
            $this->initCompilationDirectory();
        }
        return $this->compilationDirectory;
    }

    /**
     * Gets the meta data about the test definition being executed.
     * @return array
     */
    public function getTestMeta()
    {
        if (!isset($this->testMeta)) {
            $directories = $this->getCompilationDirectory();
            $data = $directories['private']->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_META_FILENAME);
            $data = str_replace('<?php', '', $data);
            $data = str_replace('?>', '', $data);
            $this->testMeta = eval($data);
        }

        return $this->testMeta;
    }

    /**
     * Gets the URI of the assessment test
     * @return string
     */
    public function getTestDefinitionUri()
    {
        return $this->testDefinitionUri;
    }

    /**
     * Gets the URI of the compiled delivery
     * @return string
     */
    public function getTestCompilationUri()
    {
        return $this->testCompilationUri;
    }

    /**
     * Gets the URI of the delivery execution
     * @return string
     */
    public function getTestExecutionUri()
    {
        return $this->testExecutionUri;
    }

    /**
     * Gets info from item index
     * @param string $id
     * @return mixed
     * @throws \common_exception_Error
     */
    public function getItemIndex($id)
    {
        return $this->itemIndex->getItem($id, \common_session_SessionManager::getSession()->getInterfaceLanguage());
    }

    /**
     * @return string
     * @throws \common_exception_Error
     */
    public function getUserUri()
    {
        if ($this->userUri === null) {
            $this->userUri = \common_session_SessionManager::getSession()->getUserUri();
        }
        return $this->userUri;
    }

    /**
     * @param string $userUri
     */
    public function setUserUri($userUri)
    {
        $this->userUri = $userUri;
    }

    /**
     * Gets a particular value from item index
     * @param string $id
     * @param string $name
     * @return mixed
     * @throws \common_exception_Error
     */
    public function getItemIndexValue($id, $name)
    {
        return $this->itemIndex->getItemValue($id, \common_session_SessionManager::getSession()->getInterfaceLanguage(), $name);
    }

    /**
     * Get Cat Engine Implementation
     *
     * Get the currently configured Cat Engine implementation.
     *
     * @return \oat\libCat\CatEngine
     */
    public function getCatEngine(RouteItem $routeItem = null)
    {
        $compiledDirectory = $this->getCompilationDirectory()['private'];
        $adaptiveSectionMap = $this->getServiceManager()->get(CatService::SERVICE_ID)->getAdaptiveSectionMap($compiledDirectory);
        $routeItem = $routeItem ? $routeItem : $this->getTestSession()->getRoute()->current();

        $sectionId = $routeItem->getAssessmentSection()->getIdentifier();
        $catEngine = false;

        if (isset($adaptiveSectionMap[$sectionId])) {
            $catEngine = $this->getServiceManager()->get(CatService::SERVICE_ID)->getEngine($adaptiveSectionMap[$sectionId]['endpoint']);
        }

        return $catEngine;
    }

    public function getTestSession()
    {
        if (!$this->testSession){
            $this->initTestSession();
        }
        return parent::getTestSession(); // TODO: Change the autogenerated stub
    }

    /**
     * Get the current CAT Session Object.
     *
     * @param RouteItem|null $routeItem
     * @return \oat\libCat\CatSession|false
     */
    public function getCatSession(RouteItem $routeItem = null)
    {
        return $this->getServiceManager()->get(CatService::SERVICE_ID)->getCatSession(
            $this->getTestSession(),
            $this->getCompilationDirectory()['private'],
            $routeItem
        );
    }

    /**
     * Persist the CAT Session Data.
     *
     * Persist the current CAT Session Data in storage.
     *
     * @param string $catSession JSON encoded CAT Session data.
     * @param RouteItem|null $routeItem
     * @return mixed
     */
    public function persistCatSession($catSession, RouteItem $routeItem = null)
    {
        return $this->getServiceManager()->get(CatService::SERVICE_ID)->persistCatSession(
            $catSession,
            $this->getTestSession(),
            $this->getCompilationDirectory()['private'],
            $routeItem
        );
    }

    /**
     * Persist seen CAT Item identifiers.
     *
     * @param string $seenCatItemId
     */
    public function persistSeenCatItemIds($seenCatItemId)
    {
        $sessionId = $this->getTestSession()->getSessionId();
        $items = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue(
            $sessionId,
            $this->getCatSection()->getSectionId(),
            'cat-seen-item-ids'
        );
        if (!$items) {
            $items = [];
        } else {
            $items = json_decode($items);
        }

        if (!in_array($seenCatItemId, $items)) {
            $items[] = $seenCatItemId;
        }

        $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue(
            $sessionId,
            $this->getCatSection()->getSectionId(),
            'cat-seen-item-ids',
            json_encode($items)
        );
    }

    /**
     * Get Last CAT Item Output.
     *
     * Get the last CAT Item Result from memory.
     */
    public function getLastCatItemOutput()
    {
        $sessionId = $this->getTestSession()->getSessionId();

        $itemOutput = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue(
            $sessionId,
            $this->getCatSection()->getSectionId(),
            'cat-item-output'
        );

        $output = [];

        if (!is_null($itemOutput)) {
            $rawData = json_decode($itemOutput, true);

            foreach ($rawData as $result) {
                /** @var ItemResult $itemResult */
                $itemResult = ItemResult::restore($result);
                $output[$itemResult->getItemRefId()] = $itemResult;
            }
        }

        return $output;
    }

    /**
     * Persist CAT Item Output.
     *
     * Persist the last CAT Item Result in memory.
     */
    public function persistLastCatItemOutput(array $lastCatItemOutput)
    {
        $sessionId = $this->getTestSession()->getSessionId();

        $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue(
            $sessionId,
            $this->getCatSection()->getSectionId(),
            'cat-item-output',
            json_encode($lastCatItemOutput)
        );
    }

    /**
     * Get Current CAT Section.
     *
     * Returns the current CatSection object. In case of the current Assessment Section is not adaptive, the method
     * returns the boolean false value.
     *
     * @return \oat\libCat\CatSection|boolean
     */
    public function getCatSection(RouteItem $routeItem = null)
    {
        return $this->getServiceManager()->get(CatService::SERVICE_ID)->getCatSection(
            $this->getTestSession(),
            $this->getCompilationDirectory()['private'],
            $routeItem
        );
    }

    /**
     * Is the Assessment Test Session Context Adaptive.
     *
     * Determines whether or not the current Assessment Test Session is in an adaptive context.
     *
     * @param AssessmentItemRef $currentAssessmentItemRef (optional) An AssessmentItemRef object to be considered as the current assessmentItemRef.
     * @return boolean
     */
    public function isAdaptive(AssessmentItemRef $currentAssessmentItemRef = null)
    {
        return $this->getServiceManager()->get(CatService::SERVICE_ID)->isAdaptive($this->getTestSession(), $currentAssessmentItemRef);
    }

    /**
     * Contains Adaptive Content.
     *
     * Whether or not the current Assessment Test Session has some adaptive contents.
     *
     * @return boolean
     */
    public function containsAdaptive()
    {
        $adaptiveSectionMap = $this->getServiceManager()->get(CatService::SERVICE_ID)->getAdaptiveSectionMap($this->getCompilationDirectory()['private']);

        return !empty($adaptiveSectionMap);
    }

    /**
     * Select the next Adaptive Item and store the retrieved results from CAT engine
     *
     * Ask the CAT Engine for the Next Item to be presented to the candidate, depending on the last
     * CAT Item ID and last CAT Item Output currently stored.
     *
     * This method returns a CAT Item ID in case of the CAT Engine returned one. Otherwise, it returns
     * null meaning that there is no CAT Item to be presented.
     *
     * @return mixed|null
     * @throws \common_Exception
     */
    public function selectAdaptiveNextItem()
    {
        $lastItemId = $this->getCurrentCatItemId();
        $lastOutput = $this->getLastCatItemOutput();
        $catSession = $this->getCatSession();

        $preSelection = $catSession->getTestMap();

        try {
            if (!$this->syncingMode) {
                $selection = $catSession->getTestMap(array_values($lastOutput));

                if (!$this->saveAdaptiveResults($catSession)) {
                    \common_Logger::w('Unable to save CatService results.');
                }
                $isShadowItem = false;
            } else {
                $selection = $catSession->getTestMap();
                $isShadowItem = true;
            }
        } catch (CatEngineException $e) {
            \common_Logger::e('Error during CatEngine processing. ' . $e->getMessage());
            $selection = $catSession->getTestMap();
            $isShadowItem = true;
        }

        $event = new SelectAdaptiveNextItemEvent($this->getTestSession(), $lastItemId, $preSelection, $selection, $isShadowItem);
        $this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);

        $this->persistCatSession($catSession);
        if (is_array($selection) && count($selection) > 0) {
            \common_Logger::d("New CAT item selection is '" . implode(', ', $selection) . "'.");
            return $selection[0];
        } else {
            \common_Logger::d('No new CAT item selection.');
            return null;
        }
    }

    /**
     * Get Current AssessmentItemRef object.
     *
     * This method returns the current AssessmentItemRef object depending on the test $context.
     *
     * @return \qtism\data\ExtendedAssessmentItemRef
     */
    public function getCurrentAssessmentItemRef()
    {
        if ($this->isAdaptive()) {
            return $this->getServiceManager()->get(CatService::SERVICE_ID)->getAssessmentItemRefByIdentifier(
                $this->getCompilationDirectory()['private'],
                $this->getCurrentCatItemId()
            );
        } else {
            return $this->getTestSession()->getCurrentAssessmentItemRef();
        }
    }

    public function getPreviouslySeenCatItemIds(RouteItem $routeItem = null)
    {
        return $this->getServiceManager()->get(CatService::SERVICE_ID)->getPreviouslySeenCatItemIds(
            $this->getTestSession(),
            $this->getCompilationDirectory()['private'],
            $routeItem
        );
    }

    public function getShadowTest(RouteItem $routeItem = null)
    {
        return $this->getServiceManager()->get(CatService::SERVICE_ID)->getShadowTest(
            $this->getTestSession(),
            $this->getCompilationDirectory()['private'],
            $routeItem
        );
    }

    public function getCurrentCatItemId(RouteItem $routeItem = null)
    {
        return $this->getServiceManager()->get(CatService::SERVICE_ID)->getCurrentCatItemId(
            $this->getTestSession(),
            $this->getCompilationDirectory()['private'],
            $routeItem
        );
    }

    public function persistCurrentCatItemId($catItemId)
    {
        $session = $this->getTestSession();
        $sessionId = $session->getSessionId();
        $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue(
            $sessionId,
            $this->getCatSection()->getSectionId(),
            'current-cat-item-id',
            $catItemId
        );

        $event = new QtiTestChangeEvent($session, new TestSessionMemento($session));
        $this->getServiceManager()->propagate($event);
        $this->getEventManager()->trigger($event);
    }

    public function getItemPositionInRoute($refId, &$catItemId = '')
    {
        $route = $this->getTestSession()->getRoute();
        $routeCount = $route->count();

        $i = 0;
        $j = 0;

        while ($i < $routeCount) {
            $routeItem = $route->getRouteItemAt($i);

            if ($this->isAdaptive($routeItem->getAssessmentItemRef())) {
                $shadow = $this->getShadowTest($routeItem);

                for ($k = 0; $k < count($shadow); $k++) {
                    if ($j == $refId) {
                        $catItemId = $shadow[$k];
                        break 2;
                    }

                    $j++;
                }
            } else {
                if ($j == $refId) {
                    break;
                }

                $j++;
            }

            $i++;
        }

        return $i;
    }

    /**
     * Get Real Current Position.
     *
     * This method returns the real position of the test taker within
     * the item flow, by considering CAT sections.
     *
     * @return integer A zero-based index.
     */
    public function getCurrentPosition()
    {
        $route = $this->getTestSession()->getRoute();
        $routeCount = $route->count();
        $routeItemPosition = $route->getPosition();
        $currentRouteItem = $route->getRouteItemAt($routeItemPosition);

        $finalPosition = 0;

        for ($i = 0; $i < $routeCount; $i++) {
            $routeItem = $route->getRouteItemAt($i);

            if ($routeItem !== $currentRouteItem) {
                if (!$this->isAdaptive($routeItem->getAssessmentItemRef())) {
                    $finalPosition++;
                } else {
                    $finalPosition += count($this->getShadowTest($routeItem));
                }
            } else {
                if ($this->isAdaptive($routeItem->getAssessmentItemRef())) {
                    $finalPosition += array_search(
                        $this->getCurrentCatItemId($routeItem),
                        $this->getShadowTest($routeItem)
                    );
                }

                break;
            }
        }

        return $finalPosition;
    }

    public function getCatAttempts($identifier, RouteItem $routeItem = null)
    {
        return $this->getServiceManager()->get(CatService::SERVICE_ID)->getCatAttempts(
            $this->getTestSession(),
            $this->getCompilationDirectory()['private'],
            $identifier,
            $routeItem
        );
    }

    public function persistCatAttempts($identifier, $attempts) {
        $sessionId = $this->getTestSession()->getSessionId();
        $sectionId = $this->getCatSection()->getSectionId();

        $catAttempts = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue(
            $sessionId,
            $sectionId,
            'cat-attempts'
        );

        $catAttempts = ($catAttempts) ? $catAttempts : [];
        $catAttempts[$identifier] = $attempts;

        $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue(
            $sessionId,
            $sectionId,
            'cat-attempts',
            $catAttempts
        );
    }

    /**
     * Can Move Backward
     *
     * Whether or not the Test Taker is able to navigate backward.
     * This implementation takes the CAT sections into consideration.
     *
     * @return boolean
     */
    public function canMoveBackward()
    {
        $moveBack = false;
        $session  = $this->getTestSession();
        if ($this->isAdaptive()) {
            $positionInCatSession = array_search(
                $this->getCurrentCatItemId(),
                $this->getShadowTest()
            );

            if ($positionInCatSession === 0) {
                // First item in cat section.
                if ($session->getRoute()->getPosition() !== 0) {
                    $moveBack = $session->getPreviousRouteItem()->getTestPart()->getNavigationMode() === NavigationMode::NONLINEAR;
                }
            } else {
                $moveBack = $session->getRoute()->current()->getTestPart()->getNavigationMode() === NavigationMode::NONLINEAR;
            }
        } else {
            $moveBack = $session->canMoveBackward();

            //check also if the sectionPause prevents you from moving backward
            if($moveBack){
                $moveBack = $this->getServiceManager()->get(SectionPauseService::SERVICE_ID)->canMoveBackward($session);
            }
        }

        return $moveBack;
    }

    /**
     * Save the Cat service result for tests and items
     *
     * @param CatSession $catSession
     * @return bool
     */
    protected function saveAdaptiveResults(CatSession $catSession)
    {
        $testResult = $catSession->getTestResult();
        $testResult = empty($testResult) ? [] : [$testResult];
        return $this->storeResult(array_merge($testResult, $catSession->getItemResults()));
    }

    /**
     * Store a Cat Result variable
     *
     * The result has to be an ItemResult and TestResult to embed CAT variables
     * After converted them to taoResultServer variables
     * Use the runner service to store the variables
     *
     * @param AbstractResult[] $results
     * @return bool
     */
    protected function storeResult(array $results)
    {
        /** @var QtiRunnerService $runnerService */
        $runnerService = $this->getServiceLocator()->get(QtiRunnerService::SERVICE_ID);

        $success = true;
        try {
            foreach ($results as $result) {

                if (!$result instanceof AbstractResult) {
                    throw new \common_Exception(__FUNCTION__ . ' requires a CAT result to store it.');
                }

                $variables = $this->convertCatVariables($result->getVariables());
                if (empty($variables)) {
                    \common_Logger::t('No Cat result variables to store.');
                    continue;
                }

                if ($result instanceof ItemResult) {
                    $itemId = $result->getItemRefId();
                    $itemUri = $this->getItemUriFromRefId($itemId);
                } else {
                    $itemUri = $itemId = null;
                    $sectionId = $this
                        ->getTestSession()
                        ->getRoute()
                        ->current()
                        ->getAssessmentSection()
                        ->getIdentifier();
                    /** @var \taoResultServer_models_classes_Variable $variable */
                    foreach ($variables as $variable) {
                        $variable->setIdentifier($sectionId . '-' . $variable->getIdentifier());
                    }
                }

                if (!$runnerService->storeVariables($this, $itemUri, $variables, $itemId)) {
                    $success = false;
                }
            }
        } catch (\Exception $e) {
            \common_Logger::w('An error has occurred during CAT result storing: ' . $e->getMessage());
            $success = false;
        }

        return $success;
    }

    /**
     * Convert CAT variables to taoResultServer variables
     *
     * Following the variable type, use the Runner service to get the appropriate variable
     * The method manage the trace, response and outcome variable
     *
     * @param array $variables
     * @return array
     * @throws \common_exception_NotImplemented If variable type is not managed
     */
    protected function convertCatVariables(array $variables)
    {
        /** @var QtiRunnerService $runnerService */
        $runnerService = $this->getServiceLocator()->get(QtiRunnerService::SERVICE_ID);
        $convertedVariables = [];

        foreach ($variables as $variable) {

            switch ($variable->getVariableType()) {
                case ResultVariable::TRACE_VARIABLE:
                    $getVariableMethod = 'getTraceVariable';
                    break;
                case ResultVariable::RESPONSE_VARIABLE:
                    $getVariableMethod = 'getResponseVariable';
                    break;
                case ResultVariable::OUTCOME_VARIABLE:
                    $getVariableMethod = 'getOutcomeVariable';
                    break;
                case ResultVariable::TEMPLATE_VARIABLE:
                default:
                    $getVariableMethod = null;
                    break;
            }

            if (is_null($getVariableMethod)) {
                \common_Logger::w('Variable of type ' . $variable->getVariableType(). ' is not implemented in ' . __METHOD__);
                throw new \common_exception_NotImplemented();
            }

            $convertedVariables[] = call_user_func_array(
                array($runnerService, $getVariableMethod), array($variable->getId(), $variable->getValue())
            );
        }

        return $convertedVariables;
    }

    /**
     * Get item uri associated to the given $itemId.
     *
     * @return string The uri
     */
    protected function getItemUriFromRefId($itemId)
    {
        $ref = $this->getServiceManager()->get(CatService::SERVICE_ID)->getAssessmentItemRefByIdentifier(
            $this->getCompilationDirectory()['private'],
            $itemId
        );
        return explode('|', $ref->getHref())[0];
    }

    /**
     * Are we in a synchronization mode
     * @return bool
     */
    public function isSyncingMode()
    {
        return $this->syncingMode;
    }

    /**
     * Set/Unset the synchronization mode
     * @param bool $syncing
     */
    public function setSyncingMode($syncing)
    {
        $this->syncingMode = (bool) $syncing;
    }

    /**
     * @return \oat\oatbox\user\User
     * @throws \common_exception_Error
     */
    private function getTestTakerFromSessionOrRds()
    {
        try{
            $session = \common_session_SessionManager::getSession();
        }catch (\common_exception_Error $exception) {
            $session = null;
            \common_Logger::w($exception->getMessage());
        }

        if ($session == null || $session->getUser() == null) {
            $testTaker = UserHelper::getUser($this->getUserUri());
        } else {
            $testTaker = $session->getUser();
        }

        return $testTaker;
    }
}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 */

namespace oat\taoQtiTest\models\runner\communicator;

use oat\oatbox\service\ServiceManagerAwareInterface;
use oat\oatbox\service\ServiceManagerAwareTrait;
use oat\taoQtiTest\models\runner\synchronisation\SynchronisationService;
use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;

class SyncChannel implements CommunicationChannel, ServiceManagerAwareInterface
{
    use ServiceManagerAwareTrait;

    const CHANNEL_NAME = 'sync';

    /**
     * Get name of channel
     *
     * @return string
     */
    public function getName()
    {
        return self::CHANNEL_NAME;
    }

    /**
     * Forward the data processing to the SynchronisationService
     *
     * @param QtiRunnerServiceContext $context
     * @param array $data
     * @return string
     */
    public function process(QtiRunnerServiceContext $context, array $data = [])
    {
        return $this->getServiceManager()->get(SynchronisationService::SERVICE_ID)->process($data, $context);
    }

}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 */

namespace oat\taoQtiTest\models\runner\synchronisation\action;

use common_Exception;
use common_exception_Error;
use common_exception_InconsistentData;
use common_Logger;
use Exception;
use oat\taoQtiTest\models\runner\synchronisation\TestRunnerAction;
use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;

/**
 * Move forward or back item into the test context.
 *
 * @package oat\taoQtiTest\models\runner\synchronisation\action
 */
class Move extends TestRunnerAction
{
    /**
     * Process the move action.
     *
     * Validate required fields.
     * Stop/Start timer and save item state.
     * Save item response and wrap the move to runner service.
     * Start next timer.
     *
     * @return array
     * @throws common_Exception
     * @throws common_exception_Error
     * @throws common_exception_InconsistentData
     */
    public function process()
    {
        $this->validate();

        $ref = ($this->getRequestParameter('ref') === false) ? null : $this->getRequestParameter('ref');
        $direction = $this->getRequestParameter('direction');
        $scope = $this->getRequestParameter('scope');
        $start = ($this->getRequestParameter('start') !== false);

        try {
            /** @var QtiRunnerServiceContext $serviceContext */
            $serviceContext = $this->getServiceContext();

            $this->saveToolStates();

            if (!$this->getRunnerService()->isTerminated($serviceContext)) {
                $this->endItemTimer($this->getTime());
                $this->saveItemState();
            }
            $this->initServiceContext();

            $this->saveItemResponses(false);

            if ($this->getRequestParameter('offline') === true) {
                $this->setOffline();
            }

            $serviceContext->getTestSession()->initItemTimer($this->getTime());
            $result = $this->getRunnerService()->move($serviceContext, $direction, $scope, $ref);

            $response = [
                'success' => $result,
            ];

            if ($result) {
                $response['testContext'] = $this->getRunnerService()->getTestContext($serviceContext);

                if ($serviceContext->containsAdaptive()) {
                    // Force map update.
                    $response['testMap'] = $this->getRunnerService()->getTestMap($serviceContext, true);
                }
            }

            common_Logger::d('Test session state : ' . $serviceContext->getTestSession()->getState());

            if ($start === true) {
                // start the timer only when move starts the item session
                // and after context build to avoid timing error
                $this->getRunnerService()->startTimer($serviceContext, $this->getTime());
            }
        } catch (Exception $e) {
            common_Logger::e($e->getMessage());
            $response = $this->getErrorResponse($e);
        }

        return $response;
    }

    /**
     * Direction and scope are required for move action.
     *
     * @return array
     */
    protected function getRequiredFields()
    {
        return array_merge(parent::getRequiredFields(), ['direction', 'scope']);
    }
}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 */

namespace oat\taoQtiTest\models\runner;

use oat\taoQtiTest\models\runner\map\QtiRunnerMap;
use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils;

trait RunnerParamParserTrait
{
    /**
     * @var QtiRunnerServiceContext
     */
    protected $serviceContext;

    /**
     * Check if the parameter associated to $name exists
     *
     * @param $name
     * @return mixed
     */
    abstract public function hasRequestParameter($name);

    /**
     * Get the parameter associated to $name
     *
     * @param $name
     * @return mixed
     */
    abstract public function getRequestParameter($name);

    /**
     * Get the Service Locator
     *
     * @return mixed
     */
    abstract public function getServiceLocator();

    /**
     * Initialize and verify the current service context
     * useful when the context was opened but not checked.
     *
     * @return boolean true if initialized
     * @throws \common_Exception
     */
    protected function initServiceContext()
    {
        $serviceContext = $this->getServiceContext();
        $this->getRunnerService()->check($serviceContext);
        return $serviceContext->init();
    }

    /**
     * @return QtiRunnerService
     */
    protected function getRunnerService()
    {
        return $this->getServiceLocator()->get(QtiRunnerService::SERVICE_ID);
    }

    /**
     * Get the Service Context
     * WARNING, there is not CSRF token check
     *
     * @return QtiRunnerServiceContext
     * @throws \common_Exception
     */
    protected function getServiceContext()
    {
        if (!$this->serviceContext) {

            $testExecution = $this->getRequestParameter('serviceCallId');
            $testDefinition  = $this->getRequestParameter('testDefinition');
            $testCompilation = $this->getRequestParameter('testCompilation');

            $this->serviceContext = $this->getRunnerService()->getServiceContext(
                $testDefinition, $testCompilation, $testExecution
            );
        }

        return $this->serviceContext;
    }

    /**
     * @param QtiRunnerServiceContext $serviceContext
     */
    public function setServiceContext($serviceContext)
    {
        $this->serviceContext = $serviceContext;
    }

    /**
     * End the item timer to QtiTimeLine

     * @param null $timestamp The start of action, optional
     * @return bool
     */
    protected function endItemTimer($timestamp = null)
    {
        if($this->getRequestParameter('itemDuration')){
            $serviceContext    = $this->getServiceContext();
            $itemDuration      = $this->getRequestParameter('itemDuration');
            return $this->getRunnerService()->endTimer($serviceContext, $itemDuration, $timestamp);
        }
        return false;
    }

    /**
     * Save the actual item state.
     * Requires params itemDefinition and itemState
     *
     * @return boolean true if saved
     * @throws \common_Exception
     */
    protected function saveItemState()
    {
        if ($this->getRequestParameter('itemDefinition') && $this->getRequestParameter('itemState')) {
            $serviceContext = $this->getServiceContext();
            $itemIdentifier = $this->getRequestParameter('itemDefinition');
            $state = $this->getRequestParameter('itemState') ? json_decode($this->getRequestParameter('itemState'), true) : new \stdClass();

            return $this->getRunnerService()->setItemState($serviceContext, $itemIdentifier, $state);
        }

        return false;
    }

    /**
     * Save the item responses
     * Requires params itemDuration and optionaly consumedExtraTime
     *
     * @param boolean $emptyAllowed if we allow empty responses
     * @return boolean true if saved
     * @throws \common_Exception
     * @throws QtiRunnerEmptyResponsesException if responses are empty, emptyAllowed is false and no allowSkipping
     */
    protected function saveItemResponses($emptyAllowed = true)
    {
        if ($this->getRequestParameter('itemDefinition') && $this->getRequestParameter('itemResponse')) {

            $itemDefinition = $this->getRequestParameter('itemDefinition');
            $serviceContext = $this->getServiceContext();
            $itemResponse = $this->getRequestParameter('itemResponse')
                ? json_decode($this->getRequestParameter('itemResponse'), true)
                : null;

            if (!is_null($itemResponse) && !empty($itemDefinition)) {
                $responses = $this
                    ->getRunnerService()
                    ->parsesItemResponse($serviceContext, $itemDefinition, $itemResponse);

                //still verify allowSkipping & empty responses
                if (
                    !$emptyAllowed
                    && $this->getRunnerService()->getTestConfig()->getConfigValue('enableAllowSkipping')
                    && !TestRunnerUtils::doesAllowSkipping($serviceContext->getTestSession())) {
                    if ($this->getRunnerService()->emptyResponse($serviceContext, $responses)) {
                        throw new QtiRunnerEmptyResponsesException();
                    }
                }

                return $this->getRunnerService()->storeItemResponse($serviceContext, $itemDefinition, $responses);
            }
        }
        return false;
    }

    /**
     * Gets the item reference for the current itemRef
     *
     * @param string $itemIdentifier the item id
     * @return string the state id
     */
    protected function getItemRef($itemIdentifier)
    {
        $serviceContext = $this->getServiceContext();
        /** @var QtiRunnerMap $mapService */
        $mapService = $this->getServiceLocator()->get(QtiRunnerMap::SERVICE_ID);
        return $mapService->getItemHref($serviceContext, $itemIdentifier);
    }
}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 */

namespace oat\taoQtiTest\models\runner\synchronisation\action;

use common_Exception;
use common_exception_Error;
use common_exception_InconsistentData;
use Exception;
use oat\taoQtiTest\models\runner\synchronisation\TestRunnerAction;

/**
 * Timeout item into the test context.
 *
 * @package oat\taoQtiTest\models\runner\synchronisation\action
 */
class Timeout extends TestRunnerAction
{
    /**
     * Process the timeout action.
     *
     * Validate required fields.
     * Stop/Start timer and save item state.
     * Save item responses and wrap the timeout to runner service.
     * Persist service context.
     * Start next timer.
     *
     * @return array
     * @throws common_Exception
     * @throws common_exception_Error
     * @throws common_exception_InconsistentData
     */
    public function process()
    {
        $this->validate();

        $ref = $this->getRequestParameter('ref') ?: null;
        $scope = $this->getRequestParameter('scope');
        $start = ($this->getRequestParameter('start') !== false);

        try {
            $serviceContext = $this->getServiceContext();

            $this->saveToolStates();

            if (!$this->getRunnerService()->isTerminated($serviceContext)) {
                $this->endItemTimer($this->getTime());
                $this->saveItemState();
            }

            $this->initServiceContext();

            $this->saveItemResponses();

            if ($this->getRequestParameter('offline') === true) {
                $this->setOffline();
            }

            $result = $this->getRunnerService()->timeout($serviceContext, $scope, $ref);

            $response = [
                'success' => $result,
            ];

            if ($result) {
                $response['testContext'] = $this->getRunnerService()->getTestContext($serviceContext);
                if ($serviceContext->containsAdaptive()) {
                    // Force map update.
                    $response['testMap'] = $this->getRunnerService()->getTestMap($serviceContext, true);
                }
            }

            if ($start == true) {
                // start the timer only when move starts the item session
                // and after context build to avoid timing error
                $this->getRunnerService()->startTimer($serviceContext, $this->getTime());
            }

        } catch (Exception $e) {
            $response = $this->getErrorResponse($e);
        }

        return $response;
    }

    /**
     * Scope is a required fields.
     *
     * @return array
     */
    protected function getRequiredFields()
    {
        return array_merge(parent::getRequiredFields(), ['scope']);
    }
}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2016 (original work) Open Assessment Technologies SA ;
 */

/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\rubric;

use oat\oatbox\service\ConfigurableService;
use oat\taoQtiTest\models\runner\RunnerServiceContext;
use OutOfBoundsException;
use qtism\data\AssessmentItemRef;
use qtism\data\View;
use qtism\runtime\tests\AssessmentTestSession;
use qtism\runtime\tests\RouteItem;
use taoQtiTest_models_classes_QtiTestService;

/**
 * Class QtiRunnerRubric
 * @package oat\taoQtiTest\models\runner\rubric
 */
class QtiRunnerRubric extends ConfigurableService implements RunnerRubric
{
    const SERVICE_ID = 'taoQtiTest/QtiRunnerRubric';

    /**
     * Gets the rubrics according to the current session state
     * The content is directly rendered into the page
     * @param RunnerServiceContext $context
     * @param AssessmentItemRef $itemRef (optional) otherwise use the current
     * @return mixed
     */
    public function getRubrics(RunnerServiceContext $context, AssessmentItemRef $itemRef = null)
    {
        /* @var AssessmentTestSession $session */
        $session = $context->getTestSession();

        $routeItem = null;
        if (!is_null($itemRef)) {
            try {
                $routeItem = $session->getRoute()->getRouteItemsByAssessmentItemRef($itemRef);
                if ($routeItem) {
                    $routeItem = $routeItem[0];
                }
            } catch (OutOfBoundsException $obe) {
                \common_Logger::d("Could not retrieve the route for item '${itemRef}'.");
            }
        } else {
            $routeItem = $session->getRoute()->current();
        }

        return implode('', $this->getRubricBlock($routeItem, $session, $context->getCompilationDirectory()));
    }

    /**
     * @param RouteItem $routeItem
     * @param AssessmentTestSession $session
     * @param array $compilationDirs
     * @return array
     */
    public function getRubricBlock($routeItem, $session, $compilationDirs)
    {
        // TODO: make a better implementation for rubrics loading.

        $rubrics = [];

        if ($routeItem) {

            $rubricRefs = $routeItem->getRubricBlockRefs();

            if (count($rubricRefs) > 0) {

                // -- variables used in the included rubric block templates.
                // base path (base URI to be used for resource inclusion).
                $basePathVarName = taoQtiTest_models_classes_QtiTestService::TEST_BASE_PATH_NAME;
                $$basePathVarName = $compilationDirs['public']->getPublicAccessUrl();

                // state name (the variable to access to get the state of the assessmentTestSession).
                $stateName = taoQtiTest_models_classes_QtiTestService::TEST_RENDERING_STATE_NAME;
                $$stateName = $session;

                // views name (the variable to be accessed for the visibility of rubric blocks).
                $viewsName = taoQtiTest_models_classes_QtiTestService::TEST_VIEWS_NAME;
                $$viewsName = array(View::CANDIDATE);

                $tmpDir = \tao_helpers_File::createTempDir();
                ob_start();
                foreach ($rubricRefs as $rubric) {
                    $data = $compilationDirs['private']->read($rubric->getHref());
                    $tmpFile = $tmpDir . basename($rubric->getHref());
                    file_put_contents($tmpFile, $data);
                    include($tmpFile);
                    unlink($tmpFile);
                }
                $rubrics[] = ob_get_contents();
                ob_end_clean();
                rmdir($tmpDir);
            }
        }

        return $rubrics;
    }
}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
 */

namespace oat\taoQtiTest\models\runner\synchronisation\action;

use common_Exception;
use common_exception_Error;
use common_exception_InconsistentData;
use Exception;
use oat\taoQtiTest\models\runner\synchronisation\TestRunnerAction;

/**
 * Pause the test
 *
 * @package oat\taoQtiTest\models\runner\synchronisation\action
 */
class Pause extends TestRunnerAction
{
    /**
     * Process the pause action.
     *
     * @return array
     * @throws common_Exception
     * @throws common_exception_Error
     * @throws common_exception_InconsistentData
     */
    public function process()
    {
        $this->validate();

        try {
            $serviceContext = $this->getServiceContext();

            $this->saveToolStates();

            if ($this->getRequestParameter('offline') === true) {
                $this->setOffline();
            }

            $result = $this->getRunnerService()->pause($serviceContext);

            $response = [
                'success' => $result
            ];
        } catch (Exception $e) {
            $response = $this->getErrorResponse($e);
        }

        return $response;
    }
}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2016 (original work) Open Assessment Technologies SA ;
 */
/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\navigation;

use oat\taoQtiTest\models\runner\RunnerServiceContext;

/**
 * Class QtiRunnerJumpItem
 * @package oat\taoQtiTest\models\runner\navigation
 */
class QtiRunnerNavigationJumpItem implements RunnerNavigation
{
    /**
     * Do the move
     * @param RunnerServiceContext $context
     * @param mixed $ref
     * @return boolean
     * @throws \common_Exception
     */
    public function move(RunnerServiceContext $context, $ref)
    {
        QtiRunnerNavigation::checkTimedSectionExit(
            $context,
            $context->getItemPositionInRoute($ref)
        );

        $catItemId = '';
        $pos = $context->getItemPositionInRoute(intval($ref), $catItemId);

        if ($context->isAdaptive()) {
            // Consider potential changes in the selected items.
            $context->selectAdaptiveNextItem();
        }

        $context->getTestSession()->jumpTo($pos);

        if ($catItemId !== '') {
            $context->persistCurrentCatItemId($catItemId);
        }

        return true;
    }
}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2016 (original work) Open Assessment Technologies SA ;
 */
/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\communicator;

use oat\oatbox\service\ConfigurableService;
use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;
use Zend\ServiceManager\ServiceLocatorAwareInterface;

/**
 * Class QtiCommunicationService
 *
 * Implements a bidirectional communication between client and server using polling
 *
 * @package oat\taoQtiTest\models\runner\communicator
 */
class QtiCommunicationService extends ConfigurableService implements CommunicationService
{
    const SERVICE_ID = 'taoQtiTest/QtiCommunicationService';

    /**
     * @deprecated use SERVICE_ID
     */
    const CONFIG_ID = 'taoQtiTest/QtiCommunicationService';

    const OPTION_CHANNELS = 'channels';

    /**
     * Processes the input messages
     * @param QtiRunnerServiceContext $context - Needs the current runner context
     * @param array $input - Accept a list of input, each one is represented by a channel's name that is a string and a message that can be any type
     * @return array - Returns a list of responses in the same order as the input list
     * @throws \common_exception_InconsistentData
     */
    public function processInput(QtiRunnerServiceContext $context, array $input)
    {
        $responses = [];

        foreach ($input as $data) {
            if (!is_array($data) || !isset($data['channel']) || !isset($data['message'])) {
                throw new \common_exception_InconsistentData('Wrong message chunk received by the bidirectional communication service: either channel or message content is missing!');
            }

            if ($this->hasChannel($data['channel'], self::CHANNEL_TYPE_INPUT)) {
                $channel = $this->getChannel($data['channel'], self::CHANNEL_TYPE_INPUT);
                // known channel, forward...
                $responses[] = $this->processChannel($channel, $context,  $data['message']);
            } else {
                // unknown channel, fallback!
                $responses[] = $this->fallback($data['channel'], $context, $data['message']);
            }
        }

        return $responses;
    }

    /**
     * Builds the list of output messages
     * @param QtiRunnerServiceContext $context - Needs the current runner context
     * @return array - Returns a list of output, each one is represented by a channel's name that is a string and a message that can be any type
     */
    public function processOutput(QtiRunnerServiceContext $context)
    {
        $messages = [];
        $channels = $this->getOption(self::OPTION_CHANNELS);
        if (is_array($channels[self::CHANNEL_TYPE_OUTPUT])) {
            foreach ($channels[self::CHANNEL_TYPE_OUTPUT] as $outputChannelName => $outputChannelClass) {
                $channel = $this->getChannel($outputChannelName, self::CHANNEL_TYPE_OUTPUT);
                $message = $this->processChannel($channel, $context);
                if ($message !== null) {
                    $messages[] = [
                        'channel' => $channel->getName(),
                        'message' => $message,
                    ];
                }
            }
        }
        return $messages;
    }

    /**
     * @param CommunicationChannel $channel
     * @param integer $channelType
     * @throws \common_exception_InconsistentData
     */
    public function attachChannel(CommunicationChannel $channel, $channelType)
    {
        if ($this->hasChannel($channel->getName(), $channelType)) {
            throw new \common_exception_InconsistentData('Channel ' . $channel->getName() . ' already registered in ' . __CLASS__);
        }

        $channels = $this->getOption(self::OPTION_CHANNELS);

        $channels[$channelType][$channel->getName()] = get_class($channel);
        $this->setOption(self::OPTION_CHANNELS, $channels);
    }

    /**
     * @param CommunicationChannel $channel
     * @param integer $channelType
     * @throws \common_exception_InconsistentData
     */
    public function detachChannel(CommunicationChannel $channel, $channelType)
    {
        if (!$this->hasChannel($channel->getName(), $channelType)) {
            throw new \common_exception_InconsistentData('Channel ' . $channel->getName() . 'is not registered in ' . __CLASS__);
        }

        $channels = $this->getOption(self::OPTION_CHANNELS);
        unset($channels[$channelType][$channel->getName()]);
        $this->setOption(self::OPTION_CHANNELS, $channels);
    }

    /**
     * Check whether channel exists
     * @param string $channelName
     * @param integer $channelType
     * @return bool
     */
    protected function hasChannel($channelName, $channelType)
    {
        $channels = $this->getOption(self::OPTION_CHANNELS);
        return isset($channels[$channelType][$channelName]);
    }

    /**
     * @param string $channelName
     * @param integer $channelType
     * @return CommunicationChannel
     */
    protected function getChannel($channelName, $channelType)
    {
        $channels = $this->getOption(self::OPTION_CHANNELS);
        $channel = new $channels[$channelType][$channelName];
        $this->propagate($channel);

        return $channel;
    }

    /**
     * @param QtiRunnerServiceContext $context
     * @param CommunicationChannel $channel
     * @param array $data
     * @throws \common_exception_InconsistentData
     * @return mixed channel response
     */
    protected function processChannel(CommunicationChannel $channel, QtiRunnerServiceContext $context, array $data = [])
    {
        return $channel->process($context, $data);
    }

    /**
     * Fallback for unknown channels
     * @param QtiRunnerServiceContext $context
     * @param string $channel
     * @param mixed $message
     * @return mixed
     */
    protected function fallback($channel, QtiRunnerServiceContext $context,  $message)
    {
        // do nothing by default, need to be overwritten
        return null;
    }
}


<?php

namespace oat\taoQtiTest\models\cat;

use qtism\data\AssessmentTest;
use qtism\data\AssessmentSection;

/**
 * Computerized Assessment Test Utilities.
 *
 * This class provide utility methods for CAT support in TAO.
 */
class CatUtils
{
    /**
     * Extract CAT Information from Test Definition.
     *
     * This method extracts CAT Information from a given $test defintion. Please find below an example
     * of return value with an adaptive section with QTI Assessment Section Identifier 'S01'.
     *
     * [
     *      'S01' =>
     *      [
     *          'adaptiveEngineRef' => 'http://somewhere.com/api',
     *          'adaptiveSettingsRef' => 'file.xml'
     *      ]
     * ]
     *
     * @param \qtism\data\AssessmentTest $test
     * @param string $namespace (optional) The namespace where to search the "adaptivity" information in the $test definition. If not given, a default namespace will be traversed.
     * @return array
     */
    public static function getCatInfo(AssessmentTest $test, $namespace = '')
    {
        if ($namespace === '') {
            $namespace = CatService::QTI_2X_ADAPTIVE_XML_NAMESPACE;
        }

        $info = [];

        foreach ($test->getComponentsByClassName('assessmentSection') as $assessmentSection) {
            if (($selection = $assessmentSection->getSelection()) !== null && (($xmlExtension = $selection->getXml())) !== null) {
                $xpath = new \DOMXPath($xmlExtension);
                $xpath->registerNamespace('ais', $namespace);

                // Reference QTI assessmentSection identifier.
                $sectionIdentifier = $assessmentSection->getIdentifier();
                $sectionInfo = [];

                // Get the adaptiveEngineRef.
                foreach ($xpath->query('.//ais:adaptiveItemSelection/ais:adaptiveEngineRef', $xmlExtension) as $adaptiveEngineRef) {
                    $sectionInfo['adaptiveEngineRef'] = $adaptiveEngineRef->getAttribute('href');
                }

                // Get the adaptiveSettingsRef.
                foreach ($xpath->query('.//ais:adaptiveItemSelection/ais:adaptiveSettingsRef', $xmlExtension) as $adaptiveSettingsRef) {
                    $sectionInfo['adaptiveSettingsRef'] = $adaptiveSettingsRef->getAttribute('href');
                }

                // Get the qtiUsagedataRef.
                foreach ($xpath->query('.//ais:adaptiveItemSelection/ais:qtiUsagedataRef', $xmlExtension) as $qtiUsagedataRef) {
                    $sectionInfo['qtiUsagedataRef'] = $qtiUsagedataRef->getAttribute('href');
                }

                // Get the qtiUsagedataRef.
                foreach ($xpath->query('.//ais:adaptiveItemSelection/ais:qtiMetadataRef', $xmlExtension) as $qtiMetadataRef) {
                    $sectionInfo['qtiMetadataRef'] = $qtiMetadataRef->getAttribute('href');
                }

                if (!empty($sectionInfo)) {
                    $info[$sectionIdentifier] = $sectionInfo;
                }
            }
        }

        return $info;
    }

    /**
     * Is a Given Section Adaptive
     *
     * This method checks whether or not a given AssessmentSection object $section is adaptive.
     *
     * @param \qtism\data\AssessmentSection $section
     * @param string $namespace (optional) The namespace where to search the "adaptivity" information in the $test definition. If not given, a default namespace will be traversed.
     *
     * @return boolean
     */
    public static function isAssessmentSectionAdaptive(AssessmentSection $section, $namespace = '')
    {
        if ($namespace === '') {
            $namespace = CatService::QTI_2X_ADAPTIVE_XML_NAMESPACE;
        }

        $isAdaptive = false;

        if (($selection = $section->getSelection()) !== null && (($xmlExtension = $selection->getXml())) !== null)
        {
            $xpath = new \DOMXPath($xmlExtension);
            $xpath->registerNamespace('ais', $namespace);

            if ($xpath->query('.//ais:adaptiveItemSelection', $xmlExtension)->length > 0) {
                $isAdaptive = true;
            }
        }

        return $isAdaptive;
    }
}


<?php
/**
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; under version 2
 * of the License (non-upgradable).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * Copyright (c) 2016 (original work) Open Assessment Technologies SA ;
 */
/**
 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
 */

namespace oat\taoQtiTest\models\runner\config;

use oat\oatbox\service\ConfigurableService;
use oat\tao\model\theme\ThemeService;
use oat\taoQtiTest\models\SectionPauseService;
use oat\taoQtiTest\models\runner\RunnerServiceContext;
use oat\taoTests\models\runner\time\TimePoint;

/**
 * Class QtiRunnerOptions
 * @package oat\taoQtiTest\models\runner\options
 */
class QtiRunnerConfig extends ConfigurableService implements RunnerConfig
{
    const SERVICE_ID = 'taoQtiTest/QtiRunnerConfig';

    const OPTION_CONFIG = 'config';

    /**
     * @deprecated since version 29.5.0, to be removed in 30.0.0. Use QtiRunnerService::TOOL_ITEM_THEME_SWITCHER instead
     */
    const TOOL_ITEM_THEME_SWITCHER = 'itemThemeSwitcher';

    /**
     * @deprecated since version 29.5.0, to be removed in 30.0.0. Use QtiRunnerService::TOOL_ITEM_THEME_SWITCHER_KEY instead
     */
    const TOOL_ITEM_THEME_SWITCHER_KEY = 'taoQtiTest/runner/plugins/tools/itemThemeSwitcher/itemThemeSwitcher';

    const TARGET_CLIENT = 'client';

    /**
     * The test runner config
     * @var array
     */
    protected $config;

    /**
     * The test runner currently activated options
     * @var array
     */
    protected $options;

    /**
     * Returns the config of the test runner
     * @return array|mixed
     * @throws \common_ext_ExtensionException
     */
    protected function buildConfig() {
        if ($this->hasOption(self::OPTION_CONFIG)) {
            // load the configuration from service
            $config = $this->getOption(self::OPTION_CONFIG);
        } else {
            // fallback to get the raw server config, using the old notation
            $rawConfig = \common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');
            // build the test config using the new notation
            $target = isset($rawConfig['timer']) && isset($rawConfig['timer']['target']) ? $rawConfig['timer']['target'] : null;
            $config = [
                'timerWarning' => isset($rawConfig['timerWarning']) ? $rawConfig['timerWarning'] : null,
                'catEngineWarning' => isset($rawConfig['catEngineWarning']) ? $rawConfig['catEngineWarning'] : null,
                'progressIndicator' => [
                    'type' => isset($rawConfig['progress-indicator']) ? $rawConfig['progress-indicator'] : null,
                    'renderer' => isset($rawConfig['progress-indicator-renderer']) ? $rawConfig['progress-indicator-renderer'] : null,
                    'scope' => isset($rawConfig['progress-indicator-scope']) ? $rawConfig['progress-indicator-scope'] : null,
                    'forced' => isset($rawConfig['progress-indicator-forced']) ? $rawConfig['progress-indicator-forced'] : false,
                    'showLabel' => !empty($rawConfig['progress-indicator-show-label']),
                    'showTotal' => !empty($rawConfig['progress-indicator-show-total']),
                    'categories' => isset($rawConfig['progress-categories']) ? $rawConfig['progress-categories'] : [],
                ],
                'review' => [
                    'enabled' => !empty($rawConfig['test-taker-review']),
                    'scope' => isset($rawConfig['test-taker-review-scope']) ? $rawConfig['test-taker-review-scope'] : null,
                    'useTitle' => !empty($rawConfig['test-taker-review-use-title']),
                    'forceTitle' => !empty($rawConfig['test-taker-review-force-title']),
                    'forceInformationalTitle' => !empty($rawConfig['test-taker-review-force-informational-title']),
                    'showLegend' => !empty($rawConfig['test-taker-review-show-legend']),
                    'defaultOpen' => !empty($rawConfig['test-taker-review-default-open']),
                    'itemTitle' => isset($rawConfig['test-taker-review-item-title']) ? $rawConfig['test-taker-review-item-title'] : null,
                    'informationalItemTitle' => isset($rawConfig['test-taker-review-informational-item-title']) ? $rawConfig['test-taker-review-informational-item-title'] : null,
                    'preventsUnseen' => !empty($rawConfig['test-taker-review-prevents-unseen']),
                    'canCollapse' => !empty($rawConfig['test-taker-review-can-collapse']),
                    'displaySubsectionTitle' => !empty($rawConfig['test-taker-review-display-subsection-title']),
                    'allowSkipahead' => isset($rawConfig['test-taker-review-skipahead']) ? $rawConfig['test-taker-review-skipahead'] : false,
                ],
                'exitButton' => !empty($rawConfig['exitButton']),
                'nextSection' => !empty($rawConfig['next-section']),
                'plugins' => isset($rawConfig['plugins']) ? $rawConfig['plugins'] : null,
                'security' => [
                    'csrfToken' => isset($rawConfig['csrf-token']) ? $rawConfig['csrf-token'] : false,
                ],
                'timer' => [
                    'target' => $target,
                    'resetAfterResume' => !empty($rawConfig['reset-timer-after-resume']),
                    'keepUpToTimeout' => !empty($rawConfig['keep-timer-up-to-timeout']),
                    'restoreTimerFromClient' => $target === self::TARGET_CLIENT,
                ],
                'enableAllowSkipping' => isset($rawConfig['enable-allow-skipping']) ? $rawConfig['enable-allow-skipping'] : false,
                'enableValidateResponses' => isset($rawConfig['enable-validate-responses']) ? $rawConfig['enable-validate-responses'] : false,
                'checkInformational' => isset($rawConfig['check-informational']) ? $rawConfig['check-informational'] : false,
                'enableUnansweredItemsWarning' => isset($rawConfig['test-taker-unanswered-items-message']) ? $rawConfig['test-taker-unanswered-items-message'] : true,
                'allowShortcuts' => !empty($rawConfig['allow-shortcuts']),
                'shortcuts' => isset($rawConfig['shortcuts']) ? $rawConfig['shortcuts'] : [],
                'itemCaching' => [
                    'enabled' => isset($rawConfig['allow-browse-next-item']) ? $rawConfig['allow-browse-next-item'] : false,
                    'amount' => isset($rawConfig['item-cache-size']) ? intval($rawConfig['item-cache-size']) : 3,
                ],
                'guidedNavigation' => isset($rawConfig['guidedNavigation']) ? $rawConfig['guidedNavigation'] : false,
                'toolStateServerStorage' => isset($rawConfig['tool-state-server-storage']) ? $rawConfig['tool-state-server-storage'] : [],
                'forceEnableLinearNextItemWarning' => isset($rawConfig['force-enable-linear-next-item-warning']) ? $rawConfig['force-enable-linear-next-item-warning'] : false,
                'enableLinearNextItemWarningCheckbox' => isset($rawConfig['enable-linear-next-item-warning-checkbox']) ? $rawConfig['enable-linear-next-item-warning-checkbox'] : true,
            ];
        }
        return $config;
    }

    /**
     * Returns the config of the test runner
     * @return mixed
     */
    public function getConfig()
    {
        if (is_null($this->config)) {
            // build the test config using the new notation
            $this->config = $this->buildConfig();
        }
        return $this->config;
    }

    /**
     * Returns the value of a config entry.
     * The name can be a namespace, each name being separated by a dot, like: 'itemCaching.enabled'
     * @param string $name
     * @return mixed
     */
    public function getConfigValue($name)
    {
        $config = $this->getConfig();

        $path = explode('.', (string)$name);
        foreach ($path as $entry) {
            if (isset($config[$entry])) {
                $config =& $config[$entry];
            } else {
                return null;
            }
        }

        return $config;
    }

    /**
     * Returns the options related to the current test context
     * @param RunnerServiceContext $context The test context
     * @return mixed
     */
    protected function buildOptions(RunnerServiceContext $context)
    {
        $session = $context->getTestSession();

        // Comment allowed? Skipping allowed? Logout or Exit allowed ?
        $options = [
            'allowComment'      => \taoQtiTest_helpers_TestRunnerUtils::doesAllowComment($session),
            'allowSkipping'     => \taoQtiTest_helpers_TestRunnerUtils::doesAllowSkipping($session),
            'exitButton'        => \taoQtiTest_helpers_TestRunnerUtils::doesAllowExit($session, $context),
            'logoutButton'      => \taoQtiTest_helpers_TestRunnerUtils::doesAllowLogout($session),
            'validateResponses' => \taoQtiTest_helpers_TestRunnerUtils::doesValidateResponses($session),
            'sectionPause'      => $this->getServiceManager()->get(SectionPauseService::SERVICE_ID)->couldBePaused($session)
        ];

        // get the options from the categories owned by the current item
        $categories = $this->getCategories($context);
        $prefixCategory = 'x-tao-option-';
        $prefixCategoryLen = strlen($prefixCategory);
        foreach ($categories as $category) {
            if (!strncmp($category, $prefixCategory, $prefixCategoryLen)) {
                // extract the option name from the category, transform to camelCase if needed
                $optionName = lcfirst(str_replace(' ', '', ucwords(strtr(substr($category, $prefixCategoryLen), ['-' => ' ', '_' => ' ']))));

                // the options added by the categories are just flags
                $options[$optionName] = true;
            }
        }

        return $options;
    }

    /**
     * Returns the options related to the current test context
     * @param RunnerServiceContext $context The test context
     * @return mixed
     */
    public function getTestOptions(RunnerServiceContext $context)
    {
        if (is_null($this->options)) {
            // build the test config using the new notation
            $this->options = $this->buildOptions($context);
        }
        return $this->options;
    }

    /**
     * Get Categories.
     *
     * Get the categories of the current AssessmentItemRef in the route depending on a given $context.
     *
     * @param RunnerServiceContext $context
     * @return array An array of strings.
     */
    protected function getCategories(RunnerServiceContext $context)
    {
        return $context->getCurrentAssessmentItemRef()->getCategories()->getArrayCopy();
    }
}