tao-test/app/taoProctoring/model/implementation/DeliveryExecutionStateService.php

659 lines
24 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 (original work) Open Assessment Technologies SA;
*
*/
namespace oat\taoProctoring\model\implementation;
use common_session_SessionManager as SessionManager;
use Context;
use oat\oatbox\event\EventManager;
use oat\oatbox\log\LoggerAwareTrait;
use oat\oatbox\mutex\LockTrait;
use oat\oatbox\user\User;
use oat\taoDelivery\model\execution\AbstractStateService;
use oat\taoDelivery\model\execution\DeliveryExecution;
use oat\taoDelivery\model\execution\ServiceProxy;
use oat\taoDelivery\models\classes\execution\event\DeliveryExecutionReactivated;
use oat\taoDeliveryRdf\model\guest\GuestTestUser;
use oat\taoProctoring\model\authorization\AuthorizationGranted;
use oat\taoProctoring\model\authorization\TestTakerAuthorizationService;
use oat\taoProctoring\model\deliveryLog\DeliveryLog;
use oat\taoProctoring\model\deliveryLog\event\DeliveryLogEvent;
use oat\taoProctoring\model\event\DeliveryExecutionFinished;
use oat\taoProctoring\model\event\DeliveryExecutionIrregularityReport;
use oat\taoProctoring\model\event\DeliveryExecutionTerminated;
use oat\taoProctoring\model\execution\DeliveryExecution as ProctoredDeliveryExecution;
use oat\taoQtiTest\models\ExtendedStateService;
use oat\taoTests\models\event\TestExecutionPausedEvent;
use qtism\runtime\tests\AssessmentTestSessionState;
use Sinergi\BrowserDetector\Browser;
use Sinergi\BrowserDetector\Os;
use Symfony\Component\Lock\Lock;
/**
* Class DeliveryExecutionStateService
* @package oat\taoProctoring\model
* @author Aleh Hutnikau <hutnikau@1pt.com>
*/
class DeliveryExecutionStateService extends AbstractStateService implements \oat\taoProctoring\model\DeliveryExecutionStateService
{
const OPTION_TERMINATION_DELAY_AFTER_PAUSE = 'termination_delay_after_pause';
/**
* @var string lifetime delivery executions in awaiting state
*/
const OPTION_CANCELLATION_DELAY = 'cancellation_delay';
const OPTION_TIME_HANDLING = 'time_handling';
const TIME_HANDLING_EXTRA_TIME = 'extra_time';
const TIME_HANDLING_TIMER_ADJUSTMENT = 'timer_adjustment';
use LoggerAwareTrait;
use LockTrait;
/**
* @var TestSessionService
*/
private $testSessionService;
/** @var Lock[] */
private $executionLocks = [];
/**
* @return array
*/
public function getDeliveriesStates()
{
return [
ProctoredDeliveryExecution::STATE_FINISHED,
ProctoredDeliveryExecution::STATE_ACTIVE,
ProctoredDeliveryExecution::STATE_PAUSED,
ProctoredDeliveryExecution::STATE_TERMINATED,
];
}
/**
* (non-PHPdoc)
* @see \oat\taoDelivery\model\execution\AbstractStateService::getInitialStatus()
*/
public function getInitialStatus($deliveryId, User $user)
{
$service = $this->getServiceLocator()->get(TestTakerAuthorizationService::SERVICE_ID);
return $service->isProctored($deliveryId, $user)
? DeliveryExecution::STATE_PAUSED
: DeliveryExecution::STATE_ACTIVE;
}
/**
* @param DeliveryExecution $deliveryExecution
* @return bool
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
*/
public function waitExecution(DeliveryExecution $deliveryExecution)
{
$result = false;
$this->lockExecution($deliveryExecution);
$executionState = $deliveryExecution->getState()->getUri();
if (
ProctoredDeliveryExecution::STATE_TERMINATED !== $executionState &&
ProctoredDeliveryExecution::STATE_FINISHED !== $executionState
) {
$this->setState($deliveryExecution, ProctoredDeliveryExecution::STATE_AWAITING);
$this->getDeliveryLogService()->log($deliveryExecution->getIdentifier(), 'TEST_AWAITING_AUTHORISATION', [
'timestamp' => microtime(true),
'context' => $this->getContext($deliveryExecution),
]);
$result = true;
}
$this->releaseExecution($deliveryExecution);
return $result;
}
/**
* Alias for self::run() (for backward capability).
*
* @param DeliveryExecution $deliveryExecution
* @return bool
*/
public function resumeExecution(DeliveryExecution $deliveryExecution)
{
return $this->run($deliveryExecution);
}
/**
* @param DeliveryExecution $deliveryExecution
* @return bool
*/
public function run(DeliveryExecution $deliveryExecution)
{
$this->lockExecution($deliveryExecution);
$session = $this->getTestSessionService()->getTestSession($deliveryExecution);
$logData = [
'web_browser_name' => $this->getBrowserDetector()->getName(),
'web_browser_version' => $this->getBrowserDetector()->getVersion(),
'os_name' => $this->getOsDetector()->getName(),
'os_version' => $this->getOsDetector()->getVersion(),
'context' => $this->getContext($deliveryExecution),
];
$this->setState($deliveryExecution, ProctoredDeliveryExecution::STATE_ACTIVE);
if ($session && $session->getState() !== AssessmentTestSessionState::INITIAL) {
$session->resume();
$this->getTestSessionService()->persist($session);
$logData['timestamp'] = microtime(true);
$this->getDeliveryLogService()->log(
$deliveryExecution->getIdentifier(),
DeliveryLogEvent::EVENT_ID_TEST_RESUME,
$logData
);
} else {
$logData['timestamp'] = microtime(true);
$this->getDeliveryLogService()->log(
$deliveryExecution->getIdentifier(),
DeliveryLogEvent::EVENT_ID_TEST_RUN,
$logData
);
}
$this->releaseExecution($deliveryExecution);
return true;
}
/**
* @param DeliveryExecution $deliveryExecution
* @param null $reason
* @param null $testCenter
* @return bool
* @throws \common_exception_Error
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
*/
public function authoriseExecution(DeliveryExecution $deliveryExecution, $reason = null, $testCenter = null)
{
$result = false;
$this->lockExecution($deliveryExecution);
if ($this->canBeAuthorised($deliveryExecution)) {
$proctor = SessionManager::getSession()->getUser();
$logData = [
'proctorUri' => $proctor->getIdentifier(),
'timestamp' => microtime(true),
];
if (!empty($reason) && is_array($reason)) {
$logData = array_merge($logData, $reason);
}
if ($testCenter !== null) {
$logData['test_center'] = $testCenter;
}
$logData['itemId'] = $this->getCurrentItemId($deliveryExecution);
$logData['context'] = $this->getContext($deliveryExecution);
$this->getDeliveryLogService()->log(
$deliveryExecution->getIdentifier(),
DeliveryLogEvent::EVENT_ID_TEST_AUTHORISE,
$logData
);
$this->setState($deliveryExecution, ProctoredDeliveryExecution::STATE_AUTHORIZED);
$eventManager = $this->getServiceLocator()->get(EventManager::SERVICE_ID);
$eventManager->trigger(new AuthorizationGranted($deliveryExecution, $proctor));
$result = true;
}
$this->releaseExecution($deliveryExecution);
return $result;
}
/**
* {@inheritDoc}
* @see \oat\taoDelivery\model\execution\StateServiceInterface::terminate()
*/
public function terminate(DeliveryExecution $deliveryExecution)
{
$this->terminateExecution($deliveryExecution);
}
/**
* Terminates a delivery execution
*
* @param DeliveryExecution $deliveryExecution
* @param null $reason
* @return bool
* @throws \common_exception_Error
* @throws \common_exception_MissingParameter
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
* @throws \qtism\runtime\storage\common\StorageException
* @throws \qtism\runtime\tests\AssessmentTestSessionException
*/
public function terminateExecution(DeliveryExecution $deliveryExecution, $reason = null)
{
$this->lockExecution($deliveryExecution);
$executionState = $deliveryExecution->getState()->getUri();
$result = false;
if (ProctoredDeliveryExecution::STATE_TERMINATED !== $executionState && ProctoredDeliveryExecution::STATE_FINISHED !== $executionState) {
$proctor = SessionManager::getSession()->getUser();
$eventManager = $this->getServiceManager()->get(EventManager::CONFIG_ID);
$session = $this->getTestSessionService()->getTestSession($deliveryExecution);
$logData = [
'reason' => $reason,
'timestamp' => microtime(true),
'context' => $this->getContext($deliveryExecution),
'itemId' => $session ? $this->getCurrentItemId($deliveryExecution) : null,
];
$this->getDeliveryLogService()->log(
$deliveryExecution->getIdentifier(),
DeliveryLogEvent::EVENT_ID_TEST_TERMINATE,
$logData
);
if ($session) {
if ($session->isRunning()) {
$session->endTestSession();
}
$this->getTestSessionService()->persist($session);
$this->getServiceLocator()->get(ExtendedStateService::SERVICE_ID)->persist($session->getSessionId());
}
// Delivery execution state changes after test session ends, in the same way as it happens
// when a human test taker takes the test.
$this->setState($deliveryExecution, ProctoredDeliveryExecution::STATE_TERMINATED);
$eventManager->trigger(new DeliveryExecutionTerminated($deliveryExecution, $proctor, $reason));
$result = true;
}
$this->releaseExecution($deliveryExecution);
return $result;
}
/**
* Alias for self::pause() (for backward capability).
*
* @param DeliveryExecution $deliveryExecution
* @param null $reason
* @return bool
* @throws \common_exception_Error
* @throws \common_exception_MissingParameter
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
* @throws \qtism\runtime\storage\common\StorageException
*/
public function pauseExecution(DeliveryExecution $deliveryExecution, $reason = null)
{
return $this->pause($deliveryExecution, $reason);
}
/**
* Pauses a delivery execution
*
* @param DeliveryExecution $deliveryExecution
* @param null $reason
* @return bool
* @throws \common_exception_Error
* @throws \common_exception_MissingParameter
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
* @throws \qtism\runtime\storage\common\StorageException
*/
public function pause(DeliveryExecution $deliveryExecution, $reason = null)
{
$this->lockExecution($deliveryExecution);
$executionState = $deliveryExecution->getState()->getUri();
$result = false;
if (ProctoredDeliveryExecution::STATE_TERMINATED !== $executionState && ProctoredDeliveryExecution::STATE_FINISHED !== $executionState) {
$session = $this->getTestSessionService()->getTestSession($deliveryExecution);
$data = [
'reason' => $reason,
'timestamp' => microtime(true),
'context' => $this->getContext($deliveryExecution),
];
$this->setState($deliveryExecution, ProctoredDeliveryExecution::STATE_PAUSED);
if ($session) {
$data['itemId'] = $this->getCurrentItemId($deliveryExecution);
if ($session->getState() !== AssessmentTestSessionState::SUSPENDED) {
$session->suspend();
$this->getTestSessionService()->persist($session);
}
$this->getServiceLocator()->get(ExtendedStateService::SERVICE_ID)->persist($session->getSessionId());
}
$this->getDeliveryLogService()->log($deliveryExecution->getIdentifier(), DeliveryLogEvent::EVENT_ID_TEST_PAUSE, $data);
$result = true;
}
$this->releaseExecution($deliveryExecution);
return $result;
}
/**
* Alias for self::finish() (for backward capability).
*
* @param DeliveryExecution $deliveryExecution
* @param null $reason
* @return bool
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
*/
public function finishExecution(DeliveryExecution $deliveryExecution, $reason = null)
{
return $this->finish($deliveryExecution, $reason);
}
/**
* @param DeliveryExecution $deliveryExecution
* @param null $reason
* @return bool
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
*/
public function finish(DeliveryExecution $deliveryExecution, $reason = null)
{
$this->lockExecution($deliveryExecution);
$result = $this->setState($deliveryExecution, ProctoredDeliveryExecution::STATE_FINISHED, $reason);
if ($result) {
$eventManager = $this->getServiceManager()->get(EventManager::SERVICE_ID);
$eventManager->trigger(new DeliveryExecutionFinished($deliveryExecution));
}
$this->releaseExecution($deliveryExecution);
return $result;
}
/**
* @param DeliveryExecution $deliveryExecution
* @param null $reason
* @return bool
* @throws \common_exception_Error
* @throws \common_exception_MissingParameter
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
*/
public function cancelExecution(DeliveryExecution $deliveryExecution, $reason = null)
{
$this->lockExecution($deliveryExecution);
$session = $this->getTestSessionService()->getTestSession($deliveryExecution);
if ($session === null) {
$data = [
'reason' => $reason,
'timestamp' => microtime(true),
'context' => $this->getContext($deliveryExecution),
];
$this->getDeliveryLogService()->log($deliveryExecution->getIdentifier(), DeliveryLogEvent::EVENT_ID_TEST_CANCEL, $data);
$result = $this->setState($deliveryExecution, ProctoredDeliveryExecution::STATE_CANCELED);
} else {
$this->logNotice('Attempt to cancel delivery execution '.$deliveryExecution->getIdentifier().' with initialized test session.');
$result = false;
}
$this->releaseExecution($deliveryExecution);
return $result;
}
/**
* @param DeliveryExecution $deliveryExecution
* @return bool
* @throws \common_exception_Error
* @throws \common_exception_MissingParameter
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
*/
public function isCancelable(DeliveryExecution $deliveryExecution)
{
return $this->getTestSessionService()->getTestSession($deliveryExecution) === null;
}
/**
* Report irregularity to a delivery execution
*
* @todo remove this method to separate service
* @param DeliveryExecution $deliveryExecution
* @param array $reason
* @return bool
*/
public function reportExecution(DeliveryExecution $deliveryExecution, $reason)
{
$deliveryLog = $this->getDeliveryLogService();
$data = [
'reason' => $reason,
'timestamp' => microtime(true),
'itemId' => $this->getCurrentItemId($deliveryExecution),
'context' => $this->getContext($deliveryExecution)
];
$returnValue = $deliveryLog->log(
$deliveryExecution->getIdentifier(),
DeliveryLogEvent::EVENT_ID_TEST_IRREGULARITY,
$data
);
// Trigger a report event.
/** @var EventManager $eventManager */
$eventManager = $this->getServiceManager()->get(EventManager::SERVICE_ID);
$eventManager->trigger(new DeliveryExecutionIrregularityReport($deliveryExecution));
return $returnValue;
}
/**
* @inheritdoc
*/
public function legacyTransition(DeliveryExecution $deliveryExecution, $state)
{
$reason = null;
$testCenter = null;
switch ($state) {
case ProctoredDeliveryExecution::STATE_ACTIVE:
$result = $this->resumeExecution($deliveryExecution);
break;
case ProctoredDeliveryExecution::STATE_AUTHORIZED:
$result = $this->authoriseExecution($deliveryExecution, $reason, $testCenter);
break;
case ProctoredDeliveryExecution::STATE_AWAITING:
$result = $this->waitExecution($deliveryExecution);
break;
case ProctoredDeliveryExecution::STATE_CANCELED:
$result = $this->cancelExecution($deliveryExecution, $reason);
break;
case ProctoredDeliveryExecution::STATE_FINISHED:
$result = $this->finishExecution($deliveryExecution, $reason);
break;
case ProctoredDeliveryExecution::STATE_PAUSED:
$result = $this->pauseExecution($deliveryExecution, $reason);
break;
case ProctoredDeliveryExecution::STATE_TERMINATED:
$result = $this->terminateExecution($deliveryExecution, $reason);
break;
default:
$this->logWarning('Unrecognised state '.$state);
$result = $this->setState($deliveryExecution, $state);
}
return $result;
}
/**
* Whether delivery execution can be moved to authorised state.
* @param DeliveryExecution $deliveryExecution
* @return bool
*/
protected function canBeAuthorised(DeliveryExecution $deliveryExecution)
{
$result = false;
$user = SessionManager::getSession()->getUser();
$stateUri = $deliveryExecution->getState()->getUri();
if ($stateUri === ProctoredDeliveryExecution::STATE_AWAITING) {
$result = true;
}
if (
$user instanceof GuestTestUser &&
!in_array($stateUri, [
ProctoredDeliveryExecution::STATE_FINISHED,
ProctoredDeliveryExecution::STATE_TERMINATED,
ProctoredDeliveryExecution::STATE_CANCELED,
])
){
$result = true;
}
return $result;
}
/**
* @return DeliveryLog
*/
private function getDeliveryLogService()
{
/** @noinspection PhpIncompatibleReturnTypeInspection */
return $this->getServiceLocator()->get(DeliveryLog::SERVICE_ID);
}
/**
* Gets test session service
*
* @return TestSessionService
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
*/
private function getTestSessionService()
{
if ($this->testSessionService === null) {
$this->testSessionService = $this->getServiceManager()->get(TestSessionService::SERVICE_ID);
}
return $this->testSessionService;
}
/**
* Get identifier of current item.
* @param DeliveryExecution $deliveryExecution
* @return null|string
*/
protected function getCurrentItemId(DeliveryExecution $deliveryExecution)
{
$result = null;
$session = $this->getTestSessionService()->getTestSession($deliveryExecution);
if ($session) {
$item = $session->getCurrentAssessmentItemRef();
if ($item) {
$result = $item->getIdentifier();
}
}
return $result;
}
/**
* Pause delivery execution if test session was paused.
* @param TestExecutionPausedEvent $event
*/
public function catchSessionPause(TestExecutionPausedEvent $event)
{
$deliveryExecution = ServiceProxy::singleton()->getDeliveryExecution($event->getTestExecutionId());
/** @var DeliveryExecutionStateService $service */
$requestParams = Context::getInstance()->getRequest()->getParameters();
$reason = null;
if (isset($requestParams['reason'])) {
$reason = $requestParams['reason'];
}
if ($deliveryExecution->getState()->getUri() !== DeliveryExecution::STATE_PAUSED) {
$this->pause($deliveryExecution, $reason);
}
}
/**
* @param DeliveryExecution $deliveryExecution
* @return string
*/
protected function getContext(DeliveryExecution $deliveryExecution)
{
$result = 'cli' === php_sapi_name()
? $_SERVER['PHP_SELF']
: Context::getInstance()->getRequest()->getRequestURI();
return $result;
}
/**
* @param DeliveryExecution $deliveryExecution
* @param null|string $reason
* @return bool
* @throws \common_exception_Error
* @throws \common_exception_NotFound
* @throws \oat\oatbox\service\exception\InvalidServiceManagerException
*/
public function reactivateExecution(DeliveryExecution $deliveryExecution, $reason = null)
{
$this->lockExecution($deliveryExecution);
$executionState = $deliveryExecution->getState()->getUri();
$result = parent::reactivateExecution($deliveryExecution, $reason);
if (ProctoredDeliveryExecution::STATE_TERMINATED === $executionState) {
$logData = [
'reason' => $reason,
'timestamp' => microtime(true),
'context' => $this->getContext($deliveryExecution),
];
$this->getDeliveryLogService()->log($deliveryExecution->getIdentifier(), DeliveryExecutionReactivated::LOG_KEY, $logData);
}
$this->releaseExecution($deliveryExecution);
return $result;
}
/**
* Get the browser detector
*
* @return Browser
*/
protected function getBrowserDetector()
{
return new Browser();
}
/**
* Get the operating system detector
*
* @return Os
*/
protected function getOsDetector()
{
return new Os();
}
/**
* @param DeliveryExecution $deliveryExecution
*/
protected function lockExecution(DeliveryExecution $deliveryExecution)
{
$deId = $deliveryExecution->getIdentifier();
$this->executionLocks[$deId] = $this->createLock(static::class.$deId, 30);
$this->executionLocks[$deId]->acquire(true);
}
/**
* @param DeliveryExecution $deliveryExecution
*/
protected function releaseExecution(DeliveryExecution $deliveryExecution)
{
$deId = $deliveryExecution->getIdentifier();
if (isset($this->executionLocks[$deId])) {
$this->executionLocks[$deId]->release();
unset($this->executionLocks[$deId]);
}
}
}