tao-test/app/taoQtiTest/actions/class.Runner.php

1161 lines
39 KiB
PHP

<?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-2020 (original work) Open Assessment Technologies SA ;
*/
/**
* @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
*
* @noinspection AutoloadingIssuesInspection
*/
use oat\libCat\exception\CatEngineConnectivityException;
use oat\oatbox\event\EventManager;
use oat\tao\model\routing\AnnotationReader\security;
use oat\taoDelivery\model\execution\DeliveryExecutionService;
use oat\taoDelivery\model\RuntimeService;
use oat\taoQtiTest\models\cat\CatEngineNotFoundException;
use oat\taoQtiTest\models\container\QtiTestDeliveryContainer;
use oat\taoQtiTest\models\event\TraceVariableStored;
use oat\taoQtiTest\models\runner\communicator\CommunicationService;
use oat\taoQtiTest\models\runner\communicator\QtiCommunicationService;
use oat\taoQtiTest\models\runner\QtiRunnerClosedException;
use oat\taoQtiTest\models\runner\QtiRunnerEmptyResponsesException;
use oat\taoQtiTest\models\runner\QtiRunnerItemResponseException;
use oat\taoQtiTest\models\runner\QtiRunnerMessageService;
use oat\taoQtiTest\models\runner\QtiRunnerPausedException;
use oat\taoQtiTest\models\runner\QtiRunnerService;
use oat\taoQtiTest\models\runner\QtiRunnerServiceContext;
use oat\taoQtiTest\models\runner\RunnerToolStates;
use oat\taoQtiTest\models\runner\StorageManager;
use taoQtiTest_helpers_TestRunnerUtils as TestRunnerUtils;
use oat\oatbox\session\SessionService;
/**
* Class taoQtiTest_actions_Runner
*
* Serves QTI implementation of the test runner
*/
class taoQtiTest_actions_Runner extends tao_actions_ServiceModule
{
use RunnerToolStates;
/**
* The current test session
* @var QtiRunnerServiceContext
*/
protected $serviceContext;
/**
* taoQtiTest_actions_Runner constructor.
* @security("hide");
*/
public function __construct()
{
parent::__construct();
// Prevent anything to be cached by the client.
TestRunnerUtils::noHttpClientCache();
}
/**
* @return StorageManager
*/
protected function getStorageManager()
{
/** @noinspection PhpIncompatibleReturnTypeInspection */
return $this->getServiceLocator()->get(StorageManager::SERVICE_ID);
}
/**
* @param $data
* @param int [$httpStatus]
* @param bool [$token]
*/
protected function returnJson($data, $httpStatus = 200)
{
try {
// auto append platform messages, if any
if ($this->serviceContext && !isset($data['messages'])) {
/* @var $communicationService CommunicationService */
$communicationService = $this->getServiceManager()->get(QtiCommunicationService::SERVICE_ID);
$data['messages'] = $communicationService->processOutput($this->serviceContext);
}
// ensure the state storage is properly updated
$this->getStorageManager()->persist();
} catch (common_Exception $e) {
$data = $this->getErrorResponse($e);
$httpStatus = $this->getErrorCode($e);
}
return parent::returnJson($data, $httpStatus);
}
/**
* Gets the identifier of the test session
* @return string
*/
protected function getSessionId()
{
if ($this->hasRequestParameter('testServiceCallId')) {
return $this->getRequestParameter('testServiceCallId');
}
return $this->getRequestParameter('serviceCallId');
}
/**
* Gets the test service context
* @return QtiRunnerServiceContext
* @throws \common_Exception
*/
protected function getServiceContext()
{
if (!$this->serviceContext) {
$testExecution = $this->getSessionId();
$execution = $this->getDeliveryExecutionService()->getDeliveryExecution($testExecution);
if (!$execution) {
throw new common_exception_ResourceNotFound();
}
$currentUser = $this->getSessionService()->getCurrentUser();
if (!$currentUser || $execution->getUserIdentifier() !== $currentUser->getIdentifier()) {
throw new common_exception_Unauthorized($execution->getUserIdentifier());
}
$delivery = $execution->getDelivery();
$container = $this->getRuntimeService()->getDeliveryContainer($delivery->getUri());
if (!$container instanceof QtiTestDeliveryContainer) {
throw new common_Exception('Non QTI test container ' . get_class($container) . ' in qti test runner');
}
$testDefinition = $container->getSourceTest($execution);
$testCompilation = $container->getPrivateDirId($execution) . '|' . $container->getPublicDirId($execution);
$this->serviceContext = $this->getRunnerService()->getServiceContext($testDefinition, $testCompilation, $testExecution);
}
return $this->serviceContext;
}
/**
* Checks the security token.
* @throws common_Exception
* @throws common_exception_Error
* @throws common_exception_Unauthorized
* @throws common_ext_ExtensionException
*/
protected function checkSecurityToken()
{
$config = $this->getRunnerService()->getTestConfig()->getConfigValue('security');
if (isset($config['csrfToken']) && $config['csrfToken'] === true) {
$this->validateCsrf();
}
}
/**
* Gets an error response object
* @param Exception [$e] Optional exception from which extract the error context
* @param array $prevResponse Response before catch
* @return array
*/
protected function getErrorResponse($e = null, $prevResponse = [])
{
$this->logError($e->getMessage());
$response = [
'success' => false,
'type' => 'error',
];
if ($e) {
if ($e instanceof Exception) {
$response['type'] = 'exception';
$response['code'] = $e->getCode();
}
if ($e instanceof common_exception_UserReadableException) {
$response['message'] = $e->getUserMessage();
} else {
$response['message'] = __('Internal server error!');
}
switch (true) {
case $e instanceof CatEngineConnectivityException:
case $e instanceof CatEngineNotFoundException:
$response = array_merge($response, $prevResponse);
$response['type'] = 'catEngine';
$response['code'] = 200;
$response['testMap'] = [];
$response['message'] = $e->getMessage();
break;
case $e instanceof QtiRunnerClosedException:
case $e instanceof QtiRunnerPausedException:
if ($this->serviceContext) {
/** @var QtiRunnerMessageService $messageService */
$messageService = $this->getServiceManager()->get(QtiRunnerMessageService::SERVICE_ID);
try {
$response['message'] = __($messageService->getStateMessage($this->serviceContext->getTestSession()));
} catch (common_exception_Error $e) {
$response['message'] = null;
}
}
$response['type'] = 'TestState';
break;
case $e instanceof tao_models_classes_FileNotFoundException:
$response['type'] = 'FileNotFound';
$response['message'] = __('File not found');
break;
case $e instanceof common_exception_Unauthorized:
$response['code'] = 403;
break;
}
}
return $response;
}
/**
* Gets an HTTP response code
* @param Exception [$e] Optional exception from which extract the error context
* @return int
*/
protected function getErrorCode($e = null)
{
$code = 200;
if ($e) {
$code = 500;
switch (true) {
case $e instanceof CatEngineConnectivityException:
case $e instanceof CatEngineNotFoundException:
case $e instanceof QtiRunnerEmptyResponsesException:
case $e instanceof QtiRunnerClosedException:
case $e instanceof QtiRunnerPausedException:
$code = 200;
break;
case $e instanceof common_exception_NotImplemented:
case $e instanceof common_exception_NoImplementation:
case $e instanceof common_exception_Unauthorized:
$code = 403;
break;
case $e instanceof tao_models_classes_FileNotFoundException:
$code = 404;
break;
}
}
return $code;
}
/**
* Initializes the delivery session
* @throws common_Exception
*/
public function init()
{
$this->checkSecurityToken();
try {
/** @var QtiRunnerServiceContext $serviceContext */
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$this->returnJson($this->getInitResponse($serviceContext));
} catch (Exception $e) {
$this->returnJson(
$this->getErrorResponse($e),
$this->getErrorCode($e)
);
}
}
/**
* Provides the test definition data
*
* @deprecated
*/
public function getTestData()
{
$code = 200;
try {
$this->checkSecurityToken();
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$response = [
'testData' => $this->getRunnerService()->getTestData($serviceContext),
'success' => true,
];
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Provides the test context object
*/
public function getTestContext()
{
$code = 200;
try {
$this->checkSecurityToken();
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$response = [
'testContext' => $this->getRunnerService()->getTestContext($serviceContext),
'success' => true,
];
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Provides the map of the test items
*/
public function getTestMap()
{
$code = 200;
try {
$this->checkSecurityToken();
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$response = [
'testMap' => $this->getRunnerService()->getTestMap($serviceContext),
'success' => true,
];
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Provides the definition data and the state for a particular item
*/
public function getItem()
{
$code = 200;
$itemIdentifier = $this->getRequestParameter('itemDefinition');
try {
$serviceContext = $this->getServiceContext();
//load item data
$response = $this->getItemData($itemIdentifier);
if (is_array($response)) {
$response['success'] = true;
} else {
// Build an appropriate failure response.
$response = [];
$response['success'] = false;
$userIdentifier = common_session_SessionManager::getSession()->getUser()->getIdentifier();
common_Logger::e("Unable to retrieve item with identifier '${itemIdentifier}' for user '${userIdentifier}'.");
}
$this->getRunnerService()->startTimer($serviceContext);
} catch (common_Exception $e) {
$userIdentifier = common_session_SessionManager::getSession()->getUser()->getIdentifier();
$msg = __CLASS__ . "::getItem(): Unable to retrieve item with identifier '${itemIdentifier}' for user '${userIdentifier}'.\n";
$msg .= "Exception of type '" . get_class($e) . "' was thrown in '" . $e->getFile() . "' l." . $e->getLine() . " with message '" . $e->getMessage() . "'.";
if ($e instanceof common_exception_Unauthorized) {
// Log as debug as not being authorized is not a "real" system error.
common_Logger::d($msg);
} else {
common_Logger::e($msg);
}
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Provides the definition data and the state for a list of items
*/
public function getNextItemData()
{
$code = 200;
$itemIdentifier = $this->getRequestParameter('itemDefinition');
if (!is_array($itemIdentifier)) {
$itemIdentifier = [$itemIdentifier];
}
try {
if (!$this->getRunnerService()->getTestConfig()->getConfigValue('itemCaching.enabled')) {
common_Logger::w("Attempt to disclose the next items without the configuration");
throw new common_exception_Unauthorized();
}
$response = [];
foreach ($itemIdentifier as $itemId) {
//load item data
$response['items'][] = $this->getItemData($itemId);
}
if (isset($response['items'])) {
$response['success'] = true;
}
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Create the item definition response for a given item
* @param string $itemIdentifier the item id
* @return array the item data
* @throws common_Exception
* @throws common_exception_Error
* @throws common_exception_InvalidArgumentType
*/
protected function getItemData($itemIdentifier)
{
$serviceContext = $this->getServiceContext();
$itemRef = $this->getRunnerService()->getItemHref($serviceContext, $itemIdentifier);
$itemData = $this->getRunnerService()->getItemData($serviceContext, $itemRef);
$baseUrl = $this->getRunnerService()->getItemPublicUrl($serviceContext, $itemRef);
$portableElements = $this->getRunnerService()->getItemPortableElements($serviceContext, $itemRef);
$itemState = $this->getRunnerService()->getItemState($serviceContext, $itemIdentifier);
if (is_null($itemState) || !count($itemState)) {
$itemState = new stdClass();
}
return [
'baseUrl' => $baseUrl,
'itemData' => $itemData,
'itemState' => $itemState,
'itemIdentifier' => $itemIdentifier,
'portableElements' => $portableElements
];
}
/**
* Save the actual item state.
* Requires params itemIdentifier and itemState
* @return boolean true if saved
* @throws \common_Exception
*/
protected function saveItemState()
{
if ($this->hasRequestParameter('itemDefinition') && $this->hasRequestParameter('itemState')) {
$serviceContext = $this->getServiceContext();
$itemIdentifier = $this->getRequestParameter('itemDefinition');
//to read JSON encoded params
$params = $this->getRequest()->getRawParameters();
$itemState = isset($params['itemState']) ? $params['itemState'] : new stdClass();
$state = json_decode($itemState, true);
return $this->getRunnerService()->setItemState($serviceContext, $itemIdentifier, $state);
}
return false;
}
/**
* End the item timer and save the duration
* Requires params itemDuration and optionaly consumedExtraTime
* @return boolean true if saved
* @throws \common_Exception
*/
protected function endItemTimer()
{
if ($this->hasRequestParameter('itemDuration')) {
$serviceContext = $this->getServiceContext();
$itemDuration = $this->getRequestParameter('itemDuration');
return $this->getRunnerService()->endTimer($serviceContext, $itemDuration);
}
return false;
}
/**
* Save the item responses
* Requires params itemDuration and optionally 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->hasRequestParameter('itemDefinition') && $this->hasRequestParameter('itemResponse')) {
$itemIdentifier = $this->getRequestParameter('itemDefinition');
$serviceContext = $this->getServiceContext();
$itemDefinition = $this->getRunnerService()->getItemHref($serviceContext, $itemIdentifier);
//to read JSON encoded params
$params = $this->getRequest()->getRawParameters();
$itemResponse = isset($params['itemResponse']) ? $params['itemResponse'] : null;
if ($serviceContext->getCurrentAssessmentItemRef()->getIdentifier() !== $itemIdentifier) {
throw new QtiRunnerItemResponseException(__('Item response identifier does not match current item'));
}
if (!is_null($itemResponse) && ! empty($itemDefinition)) {
$responses = $this->getRunnerService()->parsesItemResponse($serviceContext, $itemDefinition, json_decode($itemResponse, true));
//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;
}
/**
* Stores the state object and the response set of a particular item
*/
public function submitItem()
{
$code = 200;
$successState = false;
try {
// get the service context, but do not perform the test state check,
// as we need to store the item state whatever the test state is
$this->checkSecurityToken();
$serviceContext = $this->getServiceContext();
$itemRef = $this->getRunnerService()->getItemHref($serviceContext, $this->getRequestParameter('itemDefinition'));
if (!$this->getRunnerService()->isTerminated($serviceContext)) {
$this->endItemTimer();
$successState = $this->saveItemState();
}
$this->getRunnerService()->initServiceContext($serviceContext);
$successResponse = $this->saveItemResponses(false);
$displayFeedback = $this->getRunnerService()->displayFeedbacks($serviceContext);
$response = [
'success' => $successState && $successResponse,
'displayFeedbacks' => $displayFeedback
];
if ($displayFeedback == true) {
$response['feedbacks'] = $this->getRunnerService()->getFeedbacks($serviceContext, $itemRef);
$response['itemSession'] = $this->getRunnerService()->getItemSession($serviceContext);
}
$this->getRunnerService()->persist($serviceContext);
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Moves the current position to the provided scoped reference: item, section, part
*/
public function move()
{
$code = 200;
$ref = $this->getRequestParameter('ref');
$direction = $this->getRequestParameter('direction');
$scope = $this->getRequestParameter('scope');
$start = $this->hasRequestParameter('start');
try {
$this->checkSecurityToken();
$serviceContext = $this->getServiceContext();
if (!$this->getRunnerService()->isTerminated($serviceContext)) {
$this->endItemTimer();
$this->saveItemState();
}
$this->getRunnerService()->initServiceContext($serviceContext);
$this->saveItemResponses(false);
$this->saveToolStates();
$serviceContext->getTestSession()->initItemTimer();
$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());
$this->getRunnerService()->persist($serviceContext);
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);
}
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Skip the current position to the provided scope: item, section, part
*/
public function skip()
{
$code = 200;
$ref = $this->getRequestParameter('ref');
$scope = $this->getRequestParameter('scope');
$start = $this->hasRequestParameter('start');
try {
$this->checkSecurityToken();
/** @var QtiRunnerServiceContext $serviceContext */
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$this->saveToolStates();
$this->endItemTimer();
$result = $this->getRunnerService()->skip($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);
}
}
$this->getRunnerService()->persist($serviceContext);
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);
}
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Handles a test timeout
*/
public function timeout()
{
$code = 200;
$ref = $this->getRequestParameter('ref');
$scope = $this->getRequestParameter('scope');
$start = $this->hasRequestParameter('start');
$late = $this->hasRequestParameter('late');
try {
$this->checkSecurityToken();
$serviceContext = $this->getServiceContext();
if (!$this->getRunnerService()->isTerminated($serviceContext)) {
$this->endItemTimer();
$this->saveItemState();
}
$this->getRunnerService()->initServiceContext($serviceContext);
$this->saveItemResponses();
$this->saveToolStates();
$result = $this->getRunnerService()->timeout($serviceContext, $scope, $ref, $late);
$response = [
'success' => $result,
];
if ($result) {
$response['testContext'] = $this->getRunnerService()->getTestContext($serviceContext);
if ($serviceContext->containsAdaptive()) {
// Force map update.
$response['testMap'] = $this->getRunnerService()->getTestMap($serviceContext, true);
}
}
$this->getRunnerService()->persist($serviceContext);
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);
}
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Exits the test before its end
*/
public function exitTest()
{
$code = 200;
try {
$this->checkSecurityToken();
$serviceContext = $this->getServiceContext();
if (!$this->getRunnerService()->isTerminated($serviceContext)) {
$this->endItemTimer();
$this->saveItemState();
}
$this->getRunnerService()->initServiceContext($serviceContext);
$this->saveItemResponses();
$this->saveToolStates();
$response = [
'success' => $this->getRunnerService()->exitTest($serviceContext),
];
$this->getRunnerService()->persist($serviceContext);
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* @param bool $isTerminated
* @return bool
* @throws common_Exception
* @throws common_ext_ExtensionException
*/
private function shouldTimerStopOnPause(bool $isTerminated)
{
if (!$isTerminated) {
$timerTarget = $this->getRunnerService()->getTestConfig()->getConfigValue('timer.target');
if ($timerTarget === 'client') {
return true;
}
}
return false;
}
/**
* Sets the test in paused state
*/
public function pause()
{
$code = 200;
try {
$this->checkSecurityToken();
$serviceContext = $this->getServiceContext();
$isTerminated = (bool) $this->getRunnerService()->isTerminated($serviceContext);
if (!$isTerminated) {
$this->saveItemState();
}
if ($this->shouldTimerStopOnPause($isTerminated)) {
$this->endItemTimer();
}
$serviceContext = $this->getRunnerService()->initServiceContext($serviceContext);
$response = [
'success' => $this->getRunnerService()->pause($serviceContext),
];
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Resumes the test from paused state
*/
public function resume()
{
$code = 200;
try {
$this->checkSecurityToken();
/** @var QtiRunnerServiceContext $serviceContext */
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$result = $this->getRunnerService()->resume($serviceContext);
$response = [
'success' => $result,
];
if ($result) {
$response['testContext'] = $this->getRunnerService()->getTestContext($serviceContext);
if ($serviceContext->containsAdaptive()) {
// Force map update.
$response['testMap'] = $this->getRunnerService()->getTestMap($serviceContext, true);
}
}
$this->getRunnerService()->persist($serviceContext);
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Flag an item
*/
public function flagItem()
{
$code = 200;
try {
$this->checkSecurityToken();
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$testSession = $serviceContext->getTestSession();
if ($this->hasRequestParameter('position')) {
$itemPosition = intval($this->getRequestParameter('position'));
} else {
$itemPosition = $testSession->getRoute()->getPosition();
}
if ($this->hasRequestParameter('flag')) {
$flag = $this->getRequestParameter('flag');
if (is_numeric($flag)) {
$flag = (bool)(int)$flag;
} else {
$flag = 'false' !== strtolower($flag);
}
} else {
$flag = true;
}
TestRunnerUtils::setItemFlag($testSession, $itemPosition, $flag, $serviceContext);
$response = [
'success' => true,
];
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* Comment the test
*/
public function comment()
{
$code = 200;
$comment = $this->getRequestParameter('comment');
try {
$this->checkSecurityToken();
$serviceContext = $this->getRunnerService()->initServiceContext($this->getServiceContext());
$result = $this->getRunnerService()->comment($serviceContext, $comment);
$response = [
'success' => $result,
];
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* allow client to store information about the test, the section or the item
*/
public function storeTraceData()
{
$code = 200;
$traceData = json_decode(html_entity_decode($this->getRequestParameter('traceData')), true);
try {
$this->checkSecurityToken();
$serviceContext = $this->getServiceContext();
if ($this->hasRequestParameter('itemDefinition')) {
$itemRef = $this->getRunnerService()->getItemHref($serviceContext, $this->getRequestParameter('itemDefinition'));
} else {
$itemRef = null;
}
$stored = 0;
$size = count($traceData);
foreach ($traceData as $variableIdentifier => $variableValue) {
if ($this->getRunnerService()->storeTraceVariable($serviceContext, $itemRef, $variableIdentifier, $variableValue)) {
$stored++;
}
}
$response = [
'success' => $stored == $size
];
common_Logger::d("Stored {$stored}/{$size} trace variables");
$eventManager = $this->getServiceLocator()->get(EventManager::class);
$event = new TraceVariableStored($serviceContext->getTestSession()->getSessionId(), $traceData);
$eventManager->trigger($event);
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* The smallest telemetry signal,
* just to know the server is up.
*/
public function up()
{
$this->returnJson([
'success' => true
], 200);
}
/**
* Manage the bidirectional communication
* @throws common_Exception
* @throws common_exception_Error
* @throws common_exception_Unauthorized
* @throws common_ext_ExtensionException
*/
public function messages()
{
$code = 200;
$this->checkSecurityToken(); // will return 500 on error
// close the PHP session to prevent session overwriting and loss of security token for secured queries
session_write_close();
try {
$input = taoQtiCommon_helpers_Utils::readJsonPayload();
if (!$input) {
$input = [];
}
$serviceContext = $this->getServiceContext();
/* @var $communicationService CommunicationService */
$communicationService = $this->getServiceLocator()->get(QtiCommunicationService::SERVICE_ID);
$response = [
'responses' => $communicationService->processInput($serviceContext, $input),
'messages' => $communicationService->processOutput($serviceContext),
'success' => true,
];
} catch (common_Exception $e) {
$response = $this->getErrorResponse($e);
$code = $this->getErrorCode($e);
}
$this->returnJson($response, $code);
}
/**
* @return QtiRunnerService
*/
protected function getRunnerService()
{
/** @noinspection PhpIncompatibleReturnTypeInspection */
return $this->getServiceLocator()->get(QtiRunnerService::SERVICE_ID);
}
/**
*
* For RunnerToolStates
*
* @param $name
* @return mixed
* @throws common_exception_MissingParameter
*/
protected function getRawRequestParameter($name)
{
$parameters = $this->getRequest()->getRawParameters();
if (!array_key_exists($name, $parameters)) {
throw new common_exception_MissingParameter(sprintf('No such parameter "%s"', $name));
}
return $parameters[$name];
}
/**
* @param QtiRunnerServiceContext $serviceContext
* @return array
* @throws QtiRunnerClosedException
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
* @throws \qtism\runtime\storage\common\StorageException
* @throws common_Exception
* @throws common_exception_Error
* @throws common_exception_InvalidArgumentType
* @throws common_ext_ExtensionException
*/
protected function getInitResponse(QtiRunnerServiceContext $serviceContext)
{
if (
$this->hasRequestParameter('clientState')
&& $this->getRequestParameter('clientState') === 'paused'
) {
$this->getRunnerService()->pause($serviceContext);
$this->getRunnerService()->check($serviceContext);
}
$result = $this->getRunnerService()->init($serviceContext);
$this->getRunnerService()->persist($serviceContext);
if ($result) {
return array_merge(...[
$this->getInitSerializedResponse($serviceContext),
[ 'success' => true ],
]);
}
return [
'success' => false,
];
}
/**
* Checks the storeId request parameter and returns the last store id if set, false otherwise
*
* @param QtiRunnerServiceContext $serviceContext
* @return string|boolean
* @throws common_exception_InvalidArgumentType
*/
private function getClientStoreId(QtiRunnerServiceContext $serviceContext)
{
if (
$this->hasRequestParameter('storeId')
&& preg_match('/^[a-z0-9\-]+$/i', $this->getRequestParameter('storeId'))
) {
return $this->getRunnerService()->switchClientStoreId(
$serviceContext,
$this->getRequestParameter('storeId')
);
}
return false;
}
/**
* @param QtiRunnerServiceContext $serviceContext
* @return array
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
* @throws common_Exception
* @throws common_exception_InvalidArgumentType
* @throws common_ext_ExtensionException
*/
private function getInitSerializedResponse(QtiRunnerServiceContext $serviceContext)
{
return [
'success' => true,
'testData' => $this->getRunnerService()->getTestData($serviceContext),
'testContext' => $this->getRunnerService()->getTestContext($serviceContext),
'testMap' => $this->getRunnerService()->getTestMap($serviceContext),
'toolStates' => $this->getToolStates(),
'lastStoreId' => $this->getClientStoreId($serviceContext),
];
}
private function getSessionService(): SessionService
{
return $this->getServiceLocator()->get(SessionService::class);
}
private function getDeliveryExecutionService(): DeliveryExecutionService
{
return $this->getServiceLocator()->get(DeliveryExecutionService::SERVICE_ID);
}
private function getRuntimeService(): RuntimeService
{
return $this->getServiceLocator()->get(RuntimeService::SERVICE_ID);
}
}