45 lines
3.1 KiB
PHP
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; }
|
|
}
|