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; } }