Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 283
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
TestController
0.00% covered (danger)
0.00%
0 / 283
0.00% covered (danger)
0.00%
0 / 17
2352
0.00% covered (danger)
0.00%
0 / 1
 put
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 get
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 getUnit
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
6
 getFile
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 putUnitReview
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
20
 putReview
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 putUnitResponse
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
2
 patchState
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 putLog
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 putUnitState
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
12
 putUnitLog
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
12
 patchLock
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getCommands
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 updateTestState
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 patchCommandExecuted
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 postConnectionLost
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 stateArray2KeyValue
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/** @noinspection PhpUnhandledExceptionInspection */
4declare(strict_types=1);
5
6// TODO unit tests !
7
8use Slim\Exception\HttpException;
9use Slim\Exception\HttpForbiddenException;
10use Slim\Exception\HttpNotFoundException;
11use Slim\Exception\HttpUnauthorizedException;
12use Slim\Http\Response;
13use Slim\Http\ServerRequest as Request;
14
15class TestController extends Controller {
16  public static function put(Request $request, Response $response): Response {
17    /* @var $authToken AuthToken */
18    $authToken = $request->getAttribute('AuthToken');
19    $body = RequestBodyParser::getElementsFromRequest($request, [
20      'bookletName' => 'REQUIRED'
21    ]);
22
23    $test = self::testDAO()->getTestByPerson($authToken->getId(), $body['bookletName']);
24
25    if (!$test) {
26      $workspace = new Workspace($authToken->getWorkspaceId());
27      $bookletLabel = $workspace->getFileById('Booklet', $body['bookletName'])->getLabel();
28      $test = self::testDAO()->createTest($authToken->getId(), $body['bookletName'], $bookletLabel);
29    }
30
31    if ($test->locked) {
32      throw new HttpException($request, "Test #$test->id `$test->label` is locked.", 423);
33    }
34
35    $response->getBody()->write((string) $test->id);
36    return $response->withStatus(201);
37  }
38
39  public static function get(Request $request, Response $response): Response {
40    /* @var $authToken AuthToken */
41    $authToken = $request->getAttribute('AuthToken');     // auth 1
42    $testId = (int) $request->getAttribute('test_id');
43
44    $test = self::testDAO()->getTestById($testId);
45
46    if (!$test) {
47      throw new HttpNotFoundException($request, "Test #$testId not found");
48    }
49
50    if ($test->locked) {
51      throw new HttpException($request, "Test #$testId `$test->label` is locked.", 423);
52    }
53
54    $workspace = new Workspace($authToken->getWorkspaceId());
55    $bookletFile = $workspace->getFileById('Booklet', $test->bookletId); // 3
56
57    // TODO check for Mode::hasCapability('monitorable'))
58
59    if (!$test->running) {
60      $personSession = self::sessionDAO()->getPersonSessionByToken($authToken->getToken());
61      $message = SessionChangeMessage::session($test->id, $personSession);
62      $message->setTestState((array) $test->state, $test->bookletId);
63      self::testDAO()->setTestRunning($test->id);
64    } else {
65      $message = SessionChangeMessage::testState(
66        $authToken->getGroup(),
67        $authToken->getId(),
68        $test->id,
69        (array) $test->state,
70        $test->bookletId
71      );
72    }
73    BroadcastService::sessionChange($message);
74
75    return $response->withJson([
76      'mode' => $authToken->getMode(),
77      'laststate' => (array) $test->state,
78      'xml' => $bookletFile->getContent(),
79      'resources' => $workspace->getBookletResourcePaths($bookletFile->getName()),
80      'firstStart' => !$test->running,
81      'workspaceId' => $workspace->getId()
82    ]);
83  }
84
85  public static function getUnit(Request $request, Response $response): Response {
86    /* @var $authToken AuthToken */
87    $authToken = $request->getAttribute('AuthToken');
88    $unitName = $request->getAttribute('unit_name');
89    $unitAlias = $request->getAttribute('alias');
90    $testId = (int) $request->getAttribute('test_id');
91
92    $workspace = new Workspace($authToken->getWorkspaceId());
93    /* @var $unitFile XMLFileUnit */
94    $unitFile = $workspace->getFileById('Unit', $unitName);
95
96    if (!$unitAlias) {
97      $unitAlias = $unitName;
98    }
99
100    // TODO check if unit is (still) valid
101
102    // TODO each part could have a different type
103    $unitData = self::testDAO()->getDataParts($testId, $unitAlias);
104    $unitState = (object) self::testDAO()->getUnitState($testId, $unitAlias);
105
106    return $response->withJson([
107      'state' => $unitState,
108      'dataParts' => (object) $unitData['dataParts'],
109      'unitResponseType' => $unitData['dataType'],
110      'definition' => $unitFile->getDefinition()
111    ]);
112  }
113
114  // TODO move to separate controller bc route starts with /file , not with /test
115  public static function getFile(Request $request, Response $response, $args): Response {
116    $groupTokenString = $request->getAttribute('group_token');
117    $path = $args['path'];
118    $workspaceId = (int) $request->getAttribute('ws_id');
119
120    if (!$groupTokenString) {
121      throw new HttpUnauthorizedException($request, 'No Token given');
122    }
123    if (!self::sessionDAO()->groupTokenExists($workspaceId, $groupTokenString)) {
124      throw new HttpForbiddenException($request, 'Group-Token not valid');
125    }
126    if (!str_starts_with($path, 'Resource/')) {
127      throw new HttpForbiddenException($request, "Access to file `$path` not allowed with group-token.");
128    }
129
130    $workspace = new Workspace($workspaceId);
131    $resourceFile = $workspace->getWorkspacePath() . '/' . $path;
132
133    $res = fopen($resourceFile, 'rb');
134    if (!$res) {
135      throw new HttpNotFoundException($request, "File not found: `$path`");
136    }
137
138    header('Content-type: ' . FileExt::getMimeType($resourceFile));
139    header('Content-Length: ' . filesize($resourceFile));
140    header('X-Source: backend');
141    fpassthru($res);
142    http_response_code(200);
143    fclose($res);
144    die();
145  }
146
147  public static function putUnitReview(Request $request, Response $response): Response {
148    $testId = (int) $request->getAttribute('test_id');
149    $unitName = $request->getAttribute('unit_name');
150
151    $review = RequestBodyParser::getElementsFromRequest(
152      $request,
153      [
154        'priority' => 0, // was: p
155        'categories' => 0, // was: c
156        'entry' => 'REQUIRED',// was: e
157        'userAgent' => '',
158        'page' => null,
159        'pagelabel' => null,
160        'originalUnitId' => null
161      ],
162    );
163
164    // TODO check if unit exists in this booklet https://github.com/iqb-berlin/testcenter-iqb-php/issues/106
165
166    $priority = (is_numeric($review['priority']) and ($review['priority'] < 4) and ($review['priority'] >= 0))
167      ? (int) $review['priority']
168      : 0;
169
170    self::testDAO()->addUnitReview(
171      $testId,
172      $unitName,
173      $priority,
174      $review['categories'],
175      $review['entry'],
176      $review['userAgent'],
177      $review['originalUnitId'] ?? '',
178      $review['page'] ?? null,
179      $review['pagelabel'] ?? null,
180    );
181
182    return $response->withStatus(201);
183  }
184
185  public static function putReview(Request $request, Response $response): Response {
186    $testId = (int) $request->getAttribute('test_id');
187
188    $review = RequestBodyParser::getElementsFromRequest($request, [
189      'priority' => 0, // was: p
190      'categories' => 0, // was: c
191      'entry' => 'REQUIRED', // was: e
192      'userAgent' => ''
193    ]);
194
195    $priority = (is_numeric($review['priority']) and ($review['priority'] < 4) and ($review['priority'] >= 0))
196      ? (int) $review['priority']
197      : 0;
198
199    self::testDAO()->addTestReview($testId, $priority, $review['categories'], $review['entry'], $review['userAgent']);
200
201    return $response->withStatus(201);
202  }
203
204  public static function putUnitResponse(Request $request, Response $response): Response {
205    $testId = (int) $request->getAttribute('test_id');
206    $unitName = $request->getAttribute('unit_name');
207
208    $unitResponse = RequestBodyParser::getElementsFromRequest($request, [
209      'timeStamp' => 'REQUIRED',
210      'dataParts' => [],
211      'OriginalUnitId' => '',
212      'responseType' => 'unknown'
213    ]);
214
215    // TODO check if unit exists in this booklet https://github.com/iqb-berlin/testcenter-iqb-php/issues/106
216
217    self::testDAO()->updateDataParts(
218      $testId,
219      $unitName,
220      (array) $unitResponse['dataParts'],
221      $unitResponse['responseType'],
222      $unitResponse['timeStamp'],
223      $unitResponse['OriginalUnitId'],
224    );
225
226    return $response->withStatus(201);
227  }
228
229  public static function patchState(Request $request, Response $response): Response {
230    /* @var $authToken AuthToken */
231    $authToken = $request->getAttribute('AuthToken');
232
233    $testId = (int) $request->getAttribute('test_id');
234
235    $stateData = RequestBodyParser::getElementsFromArray($request, [
236      'key' => 'REQUIRED',
237      'content' => 'REQUIRED',
238      'timeStamp' => 'REQUIRED'
239    ]);
240
241    $statePatch = TestController::stateArray2KeyValue($stateData);
242
243    $newState = self::testDAO()->updateTestState($testId, $statePatch);
244
245    foreach ($stateData as $entry) {
246      self::testDAO()->addTestLog($testId, $entry['key'], $entry['timeStamp'], json_encode($entry['content']));
247    }
248
249    BroadcastService::sessionChange(
250      SessionChangeMessage::testState($authToken->getGroup(), $authToken->getId(), $testId, $newState)
251    );
252
253    return $response->withStatus(200);
254  }
255
256  public static function putLog(Request $request, Response $response): Response {
257    $testId = (int) $request->getAttribute('test_id');
258
259    $logData = RequestBodyParser::getElementsFromArray($request, [
260      'key' => 'REQUIRED',
261      'content' => '',
262      'timeStamp' => 'REQUIRED'
263    ]);
264
265    foreach ($logData as $entry) {
266      self::testDAO()->addTestLog($testId, $entry['key'], $entry['timeStamp'], json_encode($entry['content']));
267    }
268
269    return $response->withStatus(201);
270  }
271
272  public static function putUnitState(Request $request, Response $response): Response {
273    /* @var $authToken AuthToken */
274    $authToken = $request->getAttribute('AuthToken');
275
276    $testId = (int) $request->getAttribute('test_id');
277    $unitName = $request->getAttribute('unit_name');
278
279    // TODO check if unit exists in this booklet https://github.com/iqb-berlin/testcenter-iqb-php/issues/106
280
281    if (!is_array(JSON::decode($request->getBody()->getContents()))) {
282      // 'not being an array' is the new format
283      $stateData = RequestBodyParser::getElementsFromArray(
284        $request,
285        [
286          'key' => 'REQUIRED',
287          'content' => 'REQUIRED',
288          'timeStamp' => 'REQUIRED'
289        ],
290        'newState');
291
292      $originalUnitId = RequestBodyParser::getElementWithDefault($request, 'originalUnitId', '');
293    } else {
294      $stateData = RequestBodyParser::getElementsFromArray($request, [
295        'key' => 'REQUIRED',
296        'content' => 'REQUIRED',
297        'timeStamp' => 'REQUIRED'
298      ]);
299      $originalUnitId = '';
300    }
301
302    $statePatch = TestController::stateArray2KeyValue($stateData);
303    $newState = self::testDAO()->updateUnitState($testId, $unitName, $statePatch, $originalUnitId);
304
305    foreach ($stateData as $entry) {
306      self::testDAO()->addUnitLog(
307        $testId,
308        $unitName,
309        $entry['key'],
310        $entry['timeStamp'],
311        $entry['content'],
312        $originalUnitId
313      );
314    }
315
316    BroadcastService::sessionChange(
317      SessionChangeMessage::unitState(
318        $authToken->getGroup(),
319        $authToken->getId(),
320        $testId,
321        $unitName,
322        $newState
323      )
324    );
325
326    return $response->withStatus(200);
327  }
328
329  public static function putUnitLog(Request $request, Response $response): Response {
330    $testId = (int) $request->getAttribute('test_id');
331    $unitName = $request->getAttribute('unit_name');
332
333    // TODO check if unit exists in this booklet https://github.com/iqb-berlin/testcenter-iqb-php/issues/106
334    if (!is_array(JSON::decode($request->getBody()->getContents()))) {
335      // 'not being an array' is the new format
336      $logData = RequestBodyParser::getElementsFromArray(
337        $request,
338        [
339          'key' => 'REQUIRED',
340          'content' => '',
341          'timeStamp' => 'REQUIRED'
342        ],
343        'logEntries');
344      $originalUnitId = RequestBodyParser::getElementWithDefault($request, 'originalUnitId', '');
345    } else {
346      $logData = RequestBodyParser::getElementsFromArray($request, [
347        'key' => 'REQUIRED',
348        'content' => '',
349        'timeStamp' => 'REQUIRED'
350      ]);
351      $originalUnitId = '';
352    }
353
354    foreach ($logData as $entry) {
355      self::testDAO()->addUnitLog(
356        $testId,
357        $unitName,
358        $entry['key'],
359        $entry['timeStamp'],
360        json_encode($entry['content']),
361        $originalUnitId
362      );
363    }
364
365    return $response->withStatus(201);
366  }
367
368  public static function patchLock(Request $request, Response $response): Response {
369    /* @var $authToken AuthToken */
370    $authToken = $request->getAttribute('AuthToken');
371
372    $testId = (int) $request->getAttribute('test_id');
373
374    $lockEvent = RequestBodyParser::getElementsFromRequest($request, [
375      'timeStamp' => 'REQUIRED',
376      'message' => ''
377    ]);
378
379    self::testDAO()->lockTest($testId);
380    self::testDAO()->addTestLog($testId, $lockEvent['message'], $lockEvent['timeStamp']);
381
382    BroadcastService::sessionChange(
383      SessionChangeMessage::testState($authToken->getGroup(), $authToken->getId(), $testId, ['status' => 'locked'])
384    );
385
386    return $response->withStatus(200);
387  }
388
389  public static function getCommands(Request $request, Response $response): Response {
390    // TODO do we have to check access to test?
391    $testId = (int) $request->getAttribute('test_id');
392    $lastCommandId = RequestBodyParser::getElementWithDefault($request, 'lastCommandId', null);
393
394    $commands = self::testDAO()->getCommands($testId, $lastCommandId);
395
396    $testee = [
397      'testId' => $testId,
398      'disconnectNotificationUri' => Server::getUrl() . "/test/$testId/connection-lost"
399    ];
400    if (TestEnvironment::$testMode) {
401      $testee['disconnectNotificationUri'] .= '?testMode=' . TestEnvironment::$testMode;
402    }
403    $bsUrl = BroadcastService::registerChannel('testee', $testee);
404
405    if ($bsUrl !== null) {
406      $response = $response->withHeader('SubscribeURI', $bsUrl);
407    }
408
409    $testSession = self::testDAO()->getTestSession($testId);
410    if (isset($testSession['laststate']['CONNECTION']) && ($testSession['laststate']['CONNECTION'] == 'LOST')) {
411      self::updateTestState($testId, $testSession, 'CONNECTION', 'POLLING');
412    }
413
414    return $response->withJson($commands);
415  }
416
417  private static function updateTestState(int $testId, array $testSession, string $field, string $value): void {
418    $newState = self::testDAO()->updateTestState($testId, [$field => $value]);
419    self::testDAO()->addTestLog($testId, '"' . $field . '"', 0, $value);
420
421    $sessionChangeMessage = SessionChangeMessage::testState(
422      $testSession['group_name'],
423      (int) $testSession['person_id'],
424      $testId,
425      $newState
426    );
427    BroadcastService::sessionChange($sessionChangeMessage);
428  }
429
430  public static function patchCommandExecuted(Request $request, Response $response): Response {
431    // TODO to we have to check access to test?
432    $testId = (int) $request->getAttribute('test_id');
433    $commandId = (int) $request->getAttribute('command_id');
434
435    $changed = self::testDAO()->setCommandExecuted($testId, $commandId);
436
437    return $response->withStatus(200, $changed ? 'OK' : 'OK, was already marked as executed');
438  }
439
440  public static function postConnectionLost(Request $request, Response $response): Response {
441    $testId = (int) $request->getAttribute('test_id');
442
443    $testSession = self::testDAO()->getTestSession($testId);
444
445    if (isset($testSession['laststate']['CONNECTION']) && ($testSession['laststate']['CONNECTION'] == 'LOST')) {
446      return $response->withStatus(200, "connection already set as lost");
447    }
448
449    self::updateTestState($testId, $testSession, 'CONNECTION', 'LOST');
450
451    return $response->withStatus(200);
452  }
453
454  // TODO replace this and use proper data-class
455  private static function stateArray2KeyValue(array $stateData): array {
456    $statePatch = [];
457    foreach ($stateData as $stateEntry) {
458      $statePatch[$stateEntry['key']] = is_object($stateEntry['content'])
459        ? json_encode($stateEntry['content'])
460        : $stateEntry['content'];
461    }
462    return $statePatch;
463  }
464}