Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 283 |
|
0.00% |
0 / 17 |
CRAP | |
0.00% |
0 / 1 |
TestController | |
0.00% |
0 / 283 |
|
0.00% |
0 / 17 |
2352 | |
0.00% |
0 / 1 |
put | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
get | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
20 | |||
getUnit | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
6 | |||
getFile | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
putUnitReview | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
20 | |||
putReview | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
putUnitResponse | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
2 | |||
patchState | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
putLog | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
putUnitState | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
12 | |||
putUnitLog | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
12 | |||
patchLock | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
2 | |||
getCommands | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
updateTestState | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
patchCommandExecuted | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
postConnectionLost | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
stateArray2KeyValue | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | /** @noinspection PhpUnhandledExceptionInspection */ |
4 | declare(strict_types=1); |
5 | |
6 | // TODO unit tests ! |
7 | |
8 | use Slim\Exception\HttpException; |
9 | use Slim\Exception\HttpForbiddenException; |
10 | use Slim\Exception\HttpNotFoundException; |
11 | use Slim\Exception\HttpUnauthorizedException; |
12 | use Slim\Http\Response; |
13 | use Slim\Http\ServerRequest as Request; |
14 | |
15 | class 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 | } |