Files

45 lines
3.1 KiB
PHP

<?php
function exam_query($sql,$types='',$params=[]) { return function_exists('cq') ? cq($sql,$types,$params) : query($sql,$types,$params); }
// Caller holds a participant row lock before opening or mutating an attempt.
function exam_find($participant, $phase) {
return exam_query('SELECT * FROM rec_exam_attempt WHERE participant_id=? AND phase=? FOR UPDATE','ii',[$participant,$phase])->get_result()->fetch_assoc();
}
function exam_answers($attempt,$incoming) {
if(!is_array($incoming)) throw new InvalidArgumentException('Format jawaban tidak valid.');
$questions=json_decode($attempt['questions_json'],true); $allowed=[];
foreach($questions as $q) $allowed[(string)$q['id']]=$q;
foreach($incoming as $id=>$answer) {
if(!isset($allowed[(string)$id]) || !is_string($answer) || strlen($answer)>8000) throw new InvalidArgumentException('Jawaban atau ID soal tidak valid.');
$q=$allowed[(string)$id];
if((int)$attempt['phase']===4 && !in_array($answer,['','L','R'],true)) throw new InvalidArgumentException('Pilihan kepribadian tidak valid.');
if((int)$attempt['phase']===3 && $answer!=='' && !in_array($answer,[$q['opa'],$q['opb'],$q['opc'],$q['opd'],$q['ope']],true)) throw new InvalidArgumentException('Pilihan jawaban tidak valid.');
}
return $incoming;
}
function exam_finalize($attempt,$answers,$expired=false) {
foreach(json_decode($attempt['questions_json'],true) as $q) {
exam_query('INSERT INTO in_jawab(isoal,jawab,cek,ce2,idap,ts) VALUES(?,?,\'\',0,?,?)','isis',[(int)$q['id'],$answers[$q['id']]??'',(int)$attempt['participant_id'],date('Y-m-d H:i:s')]);
}
exam_query('UPDATE in_daper SET level=? WHERE id=?','ii',[(int)$attempt['phase'],(int)$attempt['participant_id']]);
exam_query('UPDATE rec_exam_attempt SET status=?,answers_json=?,submitted_at=NOW() WHERE id=?','ssi',[$expired?'expired':'submitted',json_encode($answers,JSON_UNESCAPED_UNICODE),(int)$attempt['id']]);
}
function exam_event($attempt,$type) {
// Client events are advisory. They never determine selection outcomes.
if(!in_array($type,['tab_hidden','window_blur','copy','cut','paste','contextmenu','page_resumed','relogin','stale_tab'],true)) return;
// Bound event storage and repeated-event spam, including forged browser reports.
exam_query('INSERT INTO rec_exam_event(attempt_id,event_type) SELECT ?,? WHERE (SELECT COUNT(*) FROM rec_exam_event WHERE attempt_id=?)<200 AND NOT EXISTS (SELECT 1 FROM rec_exam_event WHERE attempt_id=? AND event_type=? AND created_at>DATE_SUB(NOW(),INTERVAL 2 SECOND))','isiis',[(int)$attempt['id'],$type,(int)$attempt['id'],(int)$attempt['id'],$type]);
}
function exam_expire_due($participant) {
$db=function_exists('cdb')?cdb():db();
$db->begin_transaction();
try {
$p=exam_query('SELECT level FROM in_daper WHERE id=? FOR UPDATE','i',[$participant])->get_result()->fetch_assoc();
if(!$p) { $db->commit(); return false; }
$attempt=exam_find($participant,(int)$p['level']+1);
$expired=$attempt && $attempt['status']==='active' && time()>=(int)$attempt['deadline'];
if($expired) exam_finalize($attempt,json_decode($attempt['answers_json'],true),true);
$db->commit(); return (bool)$expired;
} catch(Throwable $error) { $db->rollback(); throw $error; }
}