/home/u764571690/domains/savitrfoundation.com/public_html/scholar/pages
NameSizeModeActions
cache/-0755rm
css/-0755rm
.htaccess26180644editdlrm
ack.php252830644editdlrm
bank.php190330644editdlrm
daily_quiz.php1073980644editdlrm
dashboard.php272430644editdlrm
distributed.php270430644editdlrm
help.php228580644editdlrm
institute.php169280644editdlrm
invite.php262230644editdlrm
leaderboard.php376840644editdlrm
personal.php154320644editdlrm
status.php302530644editdlrm
tasks.php710750644editdlrm
Edit: /home/u764571690/domains/savitrfoundation.com/public_html/scholar/pages/daily_quiz.php (107398B)
'/', 'domain' => '', // Empty for localhost compatibility 'secure' => false, // ✅ HTTP compatible for localhost 'httponly' => true, 'samesite' => 'Lax', 'lifetime' => 2592000 // 30 days cookie lifetime ]); session_start(); } // Generate CSRF token if not exists if (!isset($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = bin2hex(random_bytes(32)); } require_once __DIR__ . '/../db.php'; // ✅ PERFORMANCE: Override SimpleCache to use pages/cache directory for better organization class LocalSimpleCache extends SimpleCache { private static $cacheDir = __DIR__ . '/cache'; private static $defaultTTL = 300; public static function init() { if (!file_exists(self::$cacheDir)) { @mkdir(self::$cacheDir, 0755, true); } } public static function get($key) { self::init(); $file = self::$cacheDir . '/' . md5($key) . '.cache'; if (!file_exists($file)) { return false; } $data = @unserialize(file_get_contents($file)); if ($data === false) return false; if (time() > $data['expires']) { @unlink($file); return false; } return $data['value']; } public static function set($key, $value, $ttl = null) { self::init(); $ttl = $ttl ?? self::$defaultTTL; $file = self::$cacheDir . '/' . md5($key) . '.cache'; $data = [ 'value' => $value, 'expires' => time() + $ttl, 'created' => time() ]; // ✅ FIXED: Better file locking with timeout to prevent deadlocks $serialized = serialize($data); $fp = @fopen($file, 'c+'); if ($fp === false) { return false; } // Try to acquire lock with timeout (5 seconds) $lockAcquired = false; $attempts = 0; while ($attempts < 10 && !$lockAcquired) { if (flock($fp, LOCK_EX | LOCK_NB)) { $lockAcquired = true; } else { usleep(500000); // Wait 0.5 seconds $attempts++; } } if ($lockAcquired) { ftruncate($fp, 0); fwrite($fp, $serialized); flock($fp, LOCK_UN); fclose($fp); return true; } else { fclose($fp); // Fallback to simple write if lock fails (better than nothing) return @file_put_contents($file, $serialized) !== false; } } public static function delete($key) { self::init(); $file = self::$cacheDir . '/' . md5($key) . '.cache'; return @unlink($file); } } // Check if user is logged in if (!isset($_SESSION['user_logged_in']) || $_SESSION['user_logged_in'] !== true) { header("Location: ../student"); exit; } // Check if database connection is working if (!isset($pdo) || !$pdo) { // UX-safe error handling (replacing die() with proper redirect) header("Location: ../quiz_error.php?code=DB_CONNECTION_FAILED"); exit; } $userId = $_SESSION['user_id']; $userName = $_SESSION['user_name'] ?? 'Student'; // ✅ PERFORMANCE: Cache user profile check to reduce DB queries $profileCacheKey = 'user_profile_status_' . $userId; $applicant = LocalSimpleCache::get($profileCacheKey); if ($applicant === false) { $stmt = $pdo->prepare(" SELECT a.status FROM users u JOIN applicants a ON u.applicant_id = a.applicant_id WHERE u.user_id = ? "); $stmt->execute([$userId]); $applicant = $stmt->fetch(PDO::FETCH_ASSOC); LocalSimpleCache::set($profileCacheKey, $applicant ?: ['status' => null], 300); // Cache for 5 minutes } if (!$applicant || $applicant['status'] !== 'Approved') { // UX-safe error handling (replacing die() with proper message) echo "

Access Denied

You must have an approved application to take the daily quiz. Please complete your profile first.

"; exit; } /* ======================= DEVICE HASH ======================= */ $ua = $_SERVER['HTTP_USER_AGENT'] ?? ''; $lang = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? ''; $deviceHash = hash('sha256', $ua . $lang); // ✅ PRODUCTION SAFE device hash /* ======================= FETCH TODAY ACTIVE ATTEMPT ======================= */ // ✅ PERFORMANCE: Cache attempt check to reduce database load // Initialize $attempt to null to prevent undefined variable warnings $attempt = null; $attemptCacheKey = 'quiz_attempt_' . $userId . '_' . date('Y-m-d'); $cachedAttempt = LocalSimpleCache::get($attemptCacheKey); if ($cachedAttempt === false) { $stmt = $pdo->prepare(" SELECT * FROM quiz_attempts WHERE user_id = ? AND quiz_date = CURDATE() ORDER BY id DESC LIMIT 1 "); $stmt->execute([$userId]); $attempt = $stmt->fetch(PDO::FETCH_ASSOC); LocalSimpleCache::set($attemptCacheKey, $attempt ?: false, 60); // 1 minute cache for localhost } else { $attempt = $cachedAttempt ?: null; } // ✅ ADD ATTEMPT OWNERSHIP VALIDATION if ($attempt && (int)$attempt['user_id'] !== (int)$userId) { error_log("Attempt ownership violation | user=$userId attempt={$attempt['id']}"); header("Location: ../quiz_error.php?code=UNAUTHORIZED"); exit; } /* ======================= START QUIZ ======================= */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['start_quiz'])) { // ✅ IMPLEMENT DATABASE-BASED RATE LIMITING (instead of session-based) $stmt = $pdo->prepare(" SELECT COUNT(*) FROM quiz_attempts WHERE user_id = ? AND started_at > NOW() - INTERVAL 1 MINUTE "); $stmt->execute([$userId]); if ($stmt->fetchColumn() > 0) { echo "

Rate Limit Exceeded

Please wait 1 minute before starting another quiz.

"; exit; } // Validate CSRF token with hash_equals for timing attack protection if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { // UX-safe error handling echo "

Error

Invalid request. Please try again.

"; exit; } // ✅ CLEAR DEVICE WARNING SHOWN FLAG ON NEW QUIZ START // Reset the device warning shown flag for new quiz attempts unset($_SESSION['device_warning_shown']); if ($attempt) { // UX-safe error handling echo "

Quiz Already Started

Quiz already started today.

"; exit; } $questions = []; // ✅ PERFORMANCE: Cache question pool to reduce DB load $questionCacheKey = 'quiz_questions_pool_' . date('Y-m-d'); $allQuestions = LocalSimpleCache::get($questionCacheKey); if ($allQuestions === false) { // REPLACED ORDER BY RAND() with deterministic shuffle approach // First try to get today's questions $stmt = $pdo->prepare(" SELECT id FROM quiz_questions WHERE quiz_date = CURDATE() AND is_active = 1 LIMIT 200 "); $stmt->execute(); $todaysQuestions = $stmt->fetchAll(PDO::FETCH_COLUMN); // If not enough questions for today, get from any date if (count($todaysQuestions) < 15) { $needed = 15 - count($todaysQuestions); $stmt = $pdo->prepare(" SELECT id FROM quiz_questions WHERE is_active = 1 AND quiz_date != CURDATE() LIMIT 200 "); $stmt->execute(); $additionalQuestions = $stmt->fetchAll(PDO::FETCH_COLUMN); $allQuestions = array_merge($todaysQuestions, $additionalQuestions); } else { $allQuestions = $todaysQuestions; } // ✅ OPTIMIZED: Cache for 10 minutes - good for localhost testing LocalSimpleCache::set($questionCacheKey, $allQuestions, 600); } // Shuffle and select exactly 15 questions shuffle($allQuestions); $questions = array_slice($allQuestions, 0, 15); // Ensure we have exactly 15 questions if (count($questions) < 15) { // ✅ FIXED: Proper error page with full HTML layout instead of just exit // This prevents blank screen and provides better UX ?> Daily Quiz - Error

⚠️ Not Enough Questions

Sorry, there are not enough questions available in the database to start the quiz.

Please contact the administrator or try again later.

Go to Dashboard

beginTransaction(); // deactivate old attempts $pdo->prepare(" UPDATE quiz_attempts SET is_active_attempt = 0 WHERE user_id = ? AND quiz_date = CURDATE() ")->execute([$userId]); // create attempt with race condition protection $stmt = $pdo->prepare(" INSERT INTO quiz_attempts (user_id, quiz_date, started_at, expires_at, device_hash) VALUES (?, CURDATE(), NOW(), DATE_ADD(NOW(), INTERVAL 10 MINUTE), ?) "); $stmt->execute([$userId, $deviceHash]); $attemptId = $pdo->lastInsertId(); // Bulk insert for better performance - N+1 insert problem fix if (!empty($questions)) { $values = []; $params = []; foreach ($questions as $i => $qid) { $values[] = "(?, ?, ?)"; $params[] = $attemptId; $params[] = $qid; $params[] = $i + 1; } $sql = " INSERT INTO quiz_attempt_questions (attempt_id, question_id, question_order) VALUES " . implode(',', $values); $pdo->prepare($sql)->execute($params); } $pdo->commit(); // ✅ Clear attempt cache when new quiz starts LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); } catch (PDOException $e) { $pdo->rollBack(); // Check if it's a duplicate entry error if (in_array($e->getCode(), ['23000', '1062'], true)) { // UX-safe error handling echo "

Quiz Already Started

Quiz already started. Please refresh the page.

"; exit; } // Log the error and show a generic message error_log("Quiz start transaction failed: " . $e->getMessage()); echo "

Error

Failed to start quiz. Please try again.

"; exit; } header("Location: daily_quiz"); exit; } /* ======================= TAB SWITCH (AJAX) ======================= */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'tab_switch') { // ✅ SECURITY: Validate CSRF token for AJAX requests if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { http_response_code(403); exit; } // ✅ SECURITY: Re-fetch attempt from database to prevent stale data attacks (AJAX bypass prevention) $stmt = $pdo->prepare(" SELECT * FROM quiz_attempts WHERE user_id = ? AND quiz_date = CURDATE() AND status = 'started' ORDER BY id DESC LIMIT 1 "); $stmt->execute([$userId]); $currentAttempt = $stmt->fetch(PDO::FETCH_ASSOC); if (!$currentAttempt || (int)$currentAttempt['user_id'] !== (int)$userId) { http_response_code(403); exit; } // ✅ SECURITY: Server-side validation - Check if quiz expired if ($currentAttempt['expires_at'] && strtotime($currentAttempt['expires_at']) < time()) { $pdo->prepare(" UPDATE quiz_attempts SET status='disqualified', completed_at=NOW(), disqualification_reason='Quiz expired', is_active_attempt = 0 WHERE id = ? ")->execute([$currentAttempt['id']]); LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); if (isset($_SESSION['device_warning'])) { unset($_SESSION['device_warning']); } header('Content-Type: application/json'); echo json_encode(['status' => 'expired']); exit; } // ✅ SECURITY: Server-side activity validation - Ignore first visibility change (mobile noise filter) if ($currentAttempt['tab_switch_count'] == 0) { // Just update last_activity, don't increment count $pdo->prepare("UPDATE quiz_attempts SET last_activity = NOW() WHERE id = ?") ->execute([$currentAttempt['id']]); header('Content-Type: application/json'); echo json_encode(['status' => 'ok', 'ignored' => true]); exit; } // ✅ SECURITY: Increment tab switch count with server-side validation (prevents AJAX blocking bypass) $pdo->prepare(" UPDATE quiz_attempts SET tab_switch_count = tab_switch_count + 1, last_activity = NOW() WHERE id = ? AND status = 'started' ")->execute([$currentAttempt['id']]); // ✅ OPTIMIZED: Single query to get updated count $stmt = $pdo->prepare("SELECT tab_switch_count FROM quiz_attempts WHERE id = ?"); $stmt->execute([$currentAttempt['id']]); $updatedAttempt = $stmt->fetch(PDO::FETCH_ASSOC); // ✅ SECURITY: Disqualify if tab switch count >= 5 (server-side enforcement) if ($updatedAttempt && $updatedAttempt['tab_switch_count'] >= 5) { $disqualificationReason = 'Multiple tab switches detected (' . $updatedAttempt['tab_switch_count'] . ' times)'; $pdo->prepare(" UPDATE quiz_attempts SET status='disqualified', completed_at=NOW(), disqualification_reason=?, is_active_attempt = 0 WHERE id = ? ")->execute([$disqualificationReason, $currentAttempt['id']]); LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); header('Content-Type: application/json'); echo json_encode(['status' => 'disqualified', 'reason' => $disqualificationReason]); exit; } header('Content-Type: application/json'); echo json_encode(['status' => 'ok', 'count' => $updatedAttempt['tab_switch_count'] ?? 0]); exit; } /* ======================= PROCTORING EVENT BATCH (AJAX) - Ultra-Optimized for Shared Hosting ======================= */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'proctor_event_batch') { // Validate CSRF token for AJAX requests if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { http_response_code(403); exit; } $eventsJson = $_POST['events'] ?? '[]'; $events = json_decode($eventsJson, true); if (!is_array($events) || empty($events)) { echo json_encode(['status' => 'ok']); exit; } if ($attempt && $attempt['status'] === 'started') { $attemptId = $attempt['id']; $disqualificationTriggered = false; $reason = ''; // ✅ PERFORMANCE: Fetch current counts once to reduce queries $stmt = $pdo->prepare(" SELECT face_absence_count, noise_spike_count, fullscreen_exit_count, suspicious_activity_count FROM quiz_attempts WHERE id = ? "); $stmt->execute([$attemptId]); $counts = $stmt->fetch(PDO::FETCH_ASSOC); $faceCount = (int)($counts['face_absence_count'] ?? 0); $noiseCount = (int)($counts['noise_spike_count'] ?? 0); $fullscreenCount = (int)($counts['fullscreen_exit_count'] ?? 0); $suspiciousCount = (int)($counts['suspicious_activity_count'] ?? 0); // Prepare bulk insert for proctoring events $eventInsertValues = []; $eventInsertParams = []; // Process all events in batch foreach ($events as $eventType) { $shouldDisqualify = false; switch ($eventType) { case 'face_missing': $faceCount++; if ($faceCount >= 3) { $reason = 'Face absent from camera multiple times (' . $faceCount . ' events)'; $shouldDisqualify = true; } break; case 'loud_audio': $noiseCount++; if ($noiseCount >= 3) { $reason = 'Excessive microphone noise detected multiple times (' . $noiseCount . ' events)'; $shouldDisqualify = true; } break; case 'dev_tools_open': $reason = 'Developer tools opened during quiz'; $shouldDisqualify = true; break; case 'fullscreen_exit': $fullscreenCount++; if ($fullscreenCount >= 3) { $reason = 'Exited fullscreen mode multiple times (' . $fullscreenCount . ' times)'; $shouldDisqualify = true; } break; case 'suspicious_resize': $suspiciousCount++; break; } // Prepare event for bulk insert $eventInsertValues[] = "(?, ?, NOW())"; $eventInsertParams[] = $attemptId; $eventInsertParams[] = $eventType; if ($shouldDisqualify && !$disqualificationTriggered) { $disqualificationTriggered = true; break; // Stop processing more events if disqualified } } // ✅ PERFORMANCE: Single UPDATE query for all counters $pdo->prepare(" UPDATE quiz_attempts SET face_absence_count = ?, noise_spike_count = ?, fullscreen_exit_count = ?, suspicious_activity_count = ? WHERE id = ? ")->execute([$faceCount, $noiseCount, $fullscreenCount, $suspiciousCount, $attemptId]); // ✅ PERFORMANCE: Bulk insert all proctoring events at once if (!empty($eventInsertValues)) { $sql = "INSERT INTO proctoring_events (attempt_id, event_type, created_at) VALUES " . implode(',', $eventInsertValues); $pdo->prepare($sql)->execute($eventInsertParams); } // Handle disqualification if triggered if ($disqualificationTriggered) { $pdo->prepare(" UPDATE quiz_attempts SET status='disqualified', completed_at=NOW(), disqualification_reason=?, is_active_attempt = 0 WHERE id = ? ")->execute([$reason, $attemptId]); LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); } echo json_encode([ 'status' => 'ok', 'disqualified' => $disqualificationTriggered, 'reason' => $reason ]); exit; } echo json_encode(['status' => 'ok']); exit; } /* ======================= PROCTORING EVENT (AJAX) - Legacy Handler (for backward compatibility) ======================= */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'proctor_event') { // Validate CSRF token for AJAX requests if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { http_response_code(403); exit; } $eventType = $_POST['event_type'] ?? ''; if ($attempt && $attempt['status'] === 'started') { $attemptId = $attempt['id']; $disqualificationTriggered = false; $reason = ''; $shouldDisqualify = false; switch ($eventType) { case 'face_missing': // Log face absence and check if it has occurred too many times $pdo->prepare(" UPDATE quiz_attempts SET face_absence_count = COALESCE(face_absence_count, 0) + 1 WHERE id = ? ")->execute([$attemptId]); // Fetch updated count $stmt = $pdo->prepare("SELECT face_absence_count FROM quiz_attempts WHERE id = ?"); $stmt->execute([$attemptId]); $updatedCount = $stmt->fetchColumn(); // Disqualify if face is absent 3 times or more if ($updatedCount >= 3) { $reason = 'Face absent from camera multiple times (' . $updatedCount . ' events)'; $shouldDisqualify = true; } break; case 'loud_audio': // Log high noise event $pdo->prepare(" UPDATE quiz_attempts SET noise_spike_count = COALESCE(noise_spike_count, 0) + 1 WHERE id = ? ")->execute([$attemptId]); // Fetch updated count $stmt = $pdo->prepare("SELECT noise_spike_count FROM quiz_attempts WHERE id = ?"); $stmt->execute([$attemptId]); $updatedCount = $stmt->fetchColumn(); // Disqualify if noise spike detected 3 times or more if ($updatedCount >= 3) { $reason = 'Excessive microphone noise detected multiple times (' . $updatedCount . ' events)'; $shouldDisqualify = true; } break; case 'dev_tools_open': $reason = 'Developer tools opened during quiz'; $shouldDisqualify = true; break; case 'fullscreen_exit': // Log fullscreen exit and check count $pdo->prepare(" UPDATE quiz_attempts SET fullscreen_exit_count = COALESCE(fullscreen_exit_count, 0) + 1 WHERE id = ? ")->execute([$attemptId]); // Fetch updated count $stmt = $pdo->prepare("SELECT fullscreen_exit_count FROM quiz_attempts WHERE id = ?"); $stmt->execute([$attemptId]); $updatedCount = $stmt->fetchColumn(); // Disqualify if exited fullscreen 3+ times if ($updatedCount >= 3) { $reason = 'Exited fullscreen mode multiple times (' . $updatedCount . ' times)'; $shouldDisqualify = true; } break; case 'suspicious_resize': // Log suspicious window resize (potential screen sharing) $pdo->prepare(" UPDATE quiz_attempts SET suspicious_activity_count = COALESCE(suspicious_activity_count, 0) + 1 WHERE id = ? ")->execute([$attemptId]); // Don't auto-disqualify, just log for admin review break; default: // Ignore unknown events http_response_code(400); exit; } // Insert the event into proctoring_events table for admin review $pdo->prepare(" INSERT INTO proctoring_events (attempt_id, event_type, created_at) VALUES (?, ?, NOW()) ")->execute([$attemptId, $eventType]); if ($shouldDisqualify) { $pdo->prepare(" UPDATE quiz_attempts SET status='disqualified', completed_at=NOW(), disqualification_reason=?, is_active_attempt = 0 WHERE id = ? ")->execute([$reason, $attemptId]); // ✅ Clear attempt cache when disqualified LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); $disqualificationTriggered = true; } // Return JSON response for client-side action header('Content-Type: application/json'); echo json_encode([ 'status' => 'success', 'disqualified' => $disqualificationTriggered, 'reason' => $reason ]); } exit; } /* ======================= ANSWER SUBMIT (AJAX Flow Implemented) ======================= */ if ($_SERVER['REQUEST_METHOD'] === 'POST' && (isset($_POST['answer']) || isset($_POST['auto_answer']))) { $isAjax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest'); // Setup JSON response header for AJAX requests if ($isAjax) { header('Content-Type: application/json'); } // Validate CSRF token with hash_equals for timing attack protection if (!isset($_POST['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])) { error_log("Quiz answer submit failed: Invalid CSRF token"); if ($isAjax) { echo json_encode(['status' => 'error', 'message' => 'Invalid request.']); exit; } // Non-AJAX fallback: redirect to refresh header("Location: daily_quiz"); exit; } // Check if quiz has expired before processing answer if ($attempt['expires_at'] && strtotime($attempt['expires_at']) < time()) { $pdo->prepare(" UPDATE quiz_attempts SET status='disqualified', completed_at=NOW(), disqualification_reason='Quiz expired', is_active_attempt = 0 WHERE id=? ")->execute([$attempt['id']]); // ✅ Clear attempt cache when expired LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); if ($isAjax) { echo json_encode(['status' => 'expired', 'message' => 'Quiz expired due to inactivity.']); exit; } header("Location: daily_quiz"); exit; } if ( empty($_POST['aqid']) ) { if ($isAjax) { echo json_encode(['status' => 'error', 'message' => 'Missing question ID.']); exit; } header("Location: daily_quiz"); exit; } $aqid = (int) $_POST['aqid']; // Determine the answer if (!empty($_POST['auto_answer']) && $_POST['auto_answer'] === 'TIME') { $answer = 'TIME'; } elseif (!empty($_POST['answer'])) { $answer = strtoupper($_POST['answer']); } else { // If it's a manual submit (not auto_answer) but no answer is selected if ($isAjax) { echo json_encode(['status' => 'no_answer_selected', 'message' => 'Please select an option.']); exit; } // Fallback for non-AJAX user: treat as timed out (TIME) $answer = 'TIME'; } // Validate answer input if (!in_array($answer, ['A','B','C','D','TIME'], true)) { if ($isAjax) { echo json_encode(['status' => 'error', 'message' => 'Invalid answer value.']); exit; } header("Location: daily_quiz"); exit; } // Fetch question data $stmt = $pdo->prepare(" SELECT aq.question_started_at, q.correct_answer FROM quiz_attempt_questions aq JOIN quiz_questions q ON q.id = aq.question_id WHERE aq.id = ? AND aq.attempt_id = ? "); $stmt->execute([$aqid, $attempt['id']]); $row = $stmt->fetch(PDO::FETCH_ASSOC); if ($row && $row['question_started_at']) { $taken = time() - strtotime($row['question_started_at']); $isTimedOut = ($answer === 'TIME'); $correct = (!$isTimedOut && $answer === $row['correct_answer']) ? 1 : 0; try { $pdo->beginTransaction(); // ❌ ADD RACE CONDITION PROTECTION - Check if answer already submitted $stmt = $pdo->prepare(" UPDATE quiz_attempt_questions SET selected_answer = ?, is_correct = ?, time_taken = ? WHERE id = ? AND selected_answer IS NULL "); $stmt->execute([$answer, $correct, min($taken, 15), $aqid]); // ✅ PERFORMANCE: Clear question count cache after answer submission LocalSimpleCache::delete('question_count_' . $attempt['id']); $rowsAffected = $stmt->rowCount(); // ✅ FIXED: If no rows were affected, the answer was already submitted if ($rowsAffected == 0) { $pdo->commit(); // ✅ FIXED: Proper response for double submit - prevent user confusion if ($isAjax) { header('Content-Type: application/json'); echo json_encode([ 'status' => 'already_submitted', 'message' => 'This answer was already submitted. Loading next question...' ]); } else { header("Location: daily_quiz"); } exit; } // ✅ INCREMENT answered_count IN quiz_attempts $pdo->prepare(" UPDATE quiz_attempts SET answered_count = answered_count + 1 WHERE id = ? ")->execute([$attempt['id']]); // Check if there are any unanswered questions left $stmt = $pdo->prepare(" SELECT COUNT(*) as unanswered FROM quiz_attempt_questions WHERE attempt_id = ? AND selected_answer IS NULL "); $stmt->execute([$attempt['id']]); $unanswered = $stmt->fetch(PDO::FETCH_ASSOC)['unanswered']; $quizStatus = 'started'; // If no unanswered questions, complete the quiz if ($unanswered == 0) { $quizStatus = 'completed'; // finish quiz $score = $pdo->prepare(" SELECT COUNT(*) FROM quiz_attempt_questions WHERE attempt_id = ? AND is_correct = 1 "); $score->execute([$attempt['id']]); $score = $score->fetchColumn(); $pdo->prepare(" UPDATE quiz_attempts SET status='completed', completed_at=NOW(), score=?, is_active_attempt = 0 WHERE id = ? ")->execute([$score, $attempt['id']]); // ✅ Clear attempt cache when quiz completes LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); LocalSimpleCache::delete('question_count_' . $attempt['id']); // Clear question count cache // Update leaderboard $currentMonthYear = date('Y-m'); $stmt = $pdo->prepare(" SELECT id, total_score, quizzes_taken FROM quiz_leaderboard_monthly WHERE user_id = ? AND month_year = ? "); $stmt->execute([$userId, $currentMonthYear]); $leaderboardEntry = $stmt->fetch(PDO::FETCH_ASSOC); if ($leaderboardEntry) { $newTotalScore = $leaderboardEntry['total_score'] + $score; $newQuizzesTaken = $leaderboardEntry['quizzes_taken'] + 1; $pdo->prepare(" UPDATE quiz_leaderboard_monthly SET total_score = ?, quizzes_taken = ? WHERE id = ? ")->execute([$newTotalScore, $newQuizzesTaken, $leaderboardEntry['id']]); } else { $pdo->prepare(" INSERT INTO quiz_leaderboard_monthly (user_id, month_year, total_score, quizzes_taken) VALUES (?, ?, ?, 1) ")->execute([$userId, $currentMonthYear, $score]); } // ✅ Clear device warning after quiz completion if (isset($_SESSION['device_warning'])) { unset($_SESSION['device_warning']); } } $pdo->commit(); // AJAX RESPONSE (for manual submit) if ($isAjax) { // ✅ FIXED: Set JSON header before output to prevent page refresh header('Content-Type: application/json'); echo json_encode([ 'status' => 'success', 'message' => ($isTimedOut ? 'Time expired. Answer recorded.' : 'Answer submitted successfully.'), 'quiz_status' => $quizStatus ]); exit; } else { // Non-AJAX response (for auto-submit 'TIME') - needs a redirect header("Location: daily_quiz"); exit; } } catch (Exception $e) { $pdo->rollBack(); error_log("Quiz answer submit transaction failed: " . $e->getMessage()); if ($isAjax) { echo json_encode(['status' => 'error', 'message' => 'Failed to process answer. Please try again.']); exit; } else { // Non-AJAX fallback header("Location: daily_quiz"); exit; } } } else { if ($isAjax) { echo json_encode(['status' => 'error', 'message' => 'Question not found or already started.']); exit; } header("Location: daily_quiz"); exit; } } /* ======================= FETCH CURRENT QUESTION ======================= */ $currentQuestion = null; if ($attempt && $attempt['status'] === 'started') { // Check if quiz has expired if ($attempt['expires_at'] && strtotime($attempt['expires_at']) < time()) { $pdo->prepare(" UPDATE quiz_attempts SET status='disqualified', completed_at=NOW(), disqualification_reason='Quiz expired', is_active_attempt = 0 WHERE id=? ")->execute([$attempt['id']]); // ✅ Clear attempt cache when expired LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); // ✅ Clear device warning after quiz expiry if (isset($_SESSION['device_warning'])) { unset($_SESSION['device_warning']); } // UX-safe error handling echo "

Quiz Expired

Quiz expired due to inactivity.

"; exit; } // Enhanced device check with warning and optional disqualification if ($attempt['device_hash'] !== $deviceHash) { // Log the device change error_log("Quiz Device Change | user={$userId} | attempt={$attempt['id']}"); // Store device change in database for admin review $pdo->prepare(" UPDATE quiz_attempts SET device_changes = device_changes + 1 WHERE id = ? ")->execute([$attempt['id']]); // ✅ OPTIMIZED: Single query to get device_changes (was 2 queries) $stmt = $pdo->prepare("SELECT device_changes FROM quiz_attempts WHERE id = ?"); $stmt->execute([$attempt['id']]); $updatedAttempt = $stmt->fetch(PDO::FETCH_ASSOC); // Set session warning for first device change if ($updatedAttempt && $updatedAttempt['device_changes'] == 1) { $_SESSION['device_warning'] = true; } // Disqualify if device changed more than 1 time (2nd change triggers disqualification) if ($updatedAttempt && $updatedAttempt['device_changes'] >= 2) { $pdo->prepare(" UPDATE quiz_attempts SET status='disqualified', completed_at=NOW(), disqualification_reason='Multiple device changes detected', is_active_attempt = 0 WHERE id = ? ")->execute([$attempt['id']]); // ✅ Clear attempt cache when disqualified LocalSimpleCache::delete('quiz_attempt_' . $userId . '_' . date('Y-m-d')); // ✅ Clear device warning after device change disqualification if (isset($_SESSION['device_warning'])) { unset($_SESSION['device_warning']); } // UX-safe error handling echo "

Disqualified

You have been disqualified due to multiple device changes during the quiz.

"; exit; } } $stmt = $pdo->prepare(" SELECT q.*, aq.id AS aqid, aq.question_started_at FROM quiz_attempt_questions aq JOIN quiz_questions q ON q.id = aq.question_id WHERE aq.attempt_id = ? AND aq.selected_answer IS NULL ORDER BY aq.question_order ASC LIMIT 1 "); $stmt->execute([$attempt['id']]); $currentQuestion = $stmt->fetch(PDO::FETCH_ASSOC); // set question start time if ($currentQuestion && !$currentQuestion['question_started_at']) { $pdo->prepare(" UPDATE quiz_attempt_questions SET question_started_at = NOW() WHERE id = ? ")->execute([$currentQuestion['aqid']]); // ✅ OPTIMIZED: Update question_started_at directly without re-fetch (1 less query) // Just update the local variable instead of re-fetching $currentQuestion['question_started_at'] = date('Y-m-d H:i:s'); } // Server-side timer validation - check if current question has exceeded time limit if ($currentQuestion && $currentQuestion['question_started_at']) { $timeTaken = time() - strtotime($currentQuestion['question_started_at']); // Add grace period to prevent unfair disqualification due to network latency $buffer = 3; // seconds if ($timeTaken > (15 + $buffer)) { // Auto-submit the question with no points if time exceeded $stmt = $pdo->prepare(" UPDATE quiz_attempt_questions SET selected_answer = 'TIME', is_correct = 0, time_taken = 15 WHERE id = ? AND selected_answer IS NULL "); $stmt->execute([$currentQuestion['aqid']]); // Only redirect if the update was successful if ($stmt->rowCount() > 0) { // Redirect to reload the page and get the next question header("Location: daily_quiz"); exit; } } } } ?> Daily Quiz
REC | DO NOT TRY TO CHEAT

Loading...

⚠ Device change detected. Further changes may disqualify you.

Terms & Conditions

  • You must keep this tab focused during the quiz
  • Do not switch to other tabs or applications
  • Do not open developer tools or inspect elements
  • Keep your face visible to the camera at all times
  • Complete the quiz in one sitting without refreshing
  • Any violation may result in disqualification

Click the button above to start today's quiz. You'll have 15 questions to answer, with 15 seconds per question.

Please keep this tab focused and avoid switching to other tabs or applications during the quiz.

🎉 Congratulations! 🎉

Your quiz has been submitted successfully

You have successfully completed today's quiz.
Check the leaderboard to see how you rank!

Go to Scholarship Portal

You can check your results in the leaderboard section on scholarship portal

Disqualified

You have been disqualified from today's quiz. Please follow the quiz rules carefully:

You can try again tomorrow with a new set of questions.

15
Question: prepare(" SELECT COUNT(*) as answered_count FROM quiz_attempt_questions WHERE attempt_id = ? AND selected_answer IS NOT NULL "); $stmt->execute([$attempt['id']]); $answeredResult = $stmt->fetch(PDO::FETCH_ASSOC); $answered = $answeredResult['answered_count'] ?? 0; LocalSimpleCache::set($questionCountCacheKey, $answered, 30); // Cache for 30 seconds } $currentQuestionNum = $answered + 1; echo $currentQuestionNum . "/15"; ?>

Processing Complete

Your quiz is being processed. Please refresh the page in a moment if it does not update automatically.