namespace, '/' . $this->base . '/(?P[\d]+)', [ [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'get_front_quiz_by_id'], 'permission_callback' => [$this, 'check_permission'], 'args' => array( 'quiz_id' => array( 'validate_callback' => function ($param, $request, $key) { return is_numeric($param); } ), ), ], [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [$this, 'post_save_result_by_id'], 'permission_callback' => [$this, 'check_permission'], 'args' => array( 'quiz_id' => array( 'validate_callback' => function ($param, $request, $key) { return is_numeric($param); } ), ), ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/(?P[\d]+)' . '/save-quiz-attempt', [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [$this, 'post_save_quiz_attempt_by_id'], 'permission_callback' => [$this, 'check_permission'], 'args' => array( 'quiz_id' => array( 'validate_callback' => function ($param, $request, $key) { return is_numeric($param); } ), ), ], ); register_rest_route( $this->namespace, '/' . $this->base . '/(?P[\d]+)' . '/leaderboard', [ 'methods' => WP_REST_Server::READABLE, 'callback' => [$this, 'get_front_quiz_leaderboard_by_id'], 'permission_callback' => [$this, 'check_permission'], 'args' => array( 'quiz_id' => array( 'validate_callback' => function ($param, $request, $key) { return is_numeric($param); } ), ), ], ); register_rest_route( $this->namespace, '/' . $this->base . '/load-more-leaderboard/(?P[\d]+)', [ [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [$this, 'post_load_more_toplist_by_id'], 'permission_callback' => [$this, 'check_permission'], 'args' => array( 'quiz_id' => array( 'validate_callback' => function ($param, $request, $key) { return is_numeric($param); } ), ), ], ] ); register_rest_route( $this->namespace, '/' . $this->base . '/(?P[\d]+)/check-quiz', [ [ 'methods' => WP_REST_Server::EDITABLE, 'callback' => [$this, 'post_check_quiz_by_id'], 'permission_callback' => [$this, 'check_permission'], 'args' => array( 'quiz_id' => array( 'validate_callback' => function ($param, $request, $key) { return is_numeric($param); } ), ), ], ] ); } public function get_front_quiz_by_id($request) { // 1) Pre-parameter filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_get_front_quiz_by_id_params', $request->get_params(), $request); $res = []; $quiz_id = $params['quiz_id'] ?? $request['quiz_id'] ?? $request->get_param('quiz_id'); $user_id = get_current_user_id() ?? 0; $user_token = $params['user_token'] ?? $request->get_param('user_token') ?? ''; // Validate required fields if (empty($quiz_id)) { return new WP_Error( 'missing_id', __('Quiz id is required.', 'acadlix'), ['status' => 400] ); } // 2) Payload/Data Filter: normalize values before core execution $payload = apply_filters('acadlix_get_front_quiz_by_id_payload', [ 'quiz_id' => intval($quiz_id), 'user_id' => intval($user_id), 'user_token' => $user_token, ], $params); $appends = apply_filters('acadlix_get_front_quiz_by_id_quiz_appends', [ 'rendered_post_content', 'rendered_metas', 'category', 'languages', 'rendered_questions', ], $payload['quiz_id'], $params); $quiz = acadlix()->model()->quiz()->ofQuiz()->find($payload['quiz_id'])->setAppends($appends); $quiz_attempt = null; if ($payload['user_id'] > 0) { $quiz_attempt = acadlix()->model()->userActivityMeta()->ofQuiz() ->ofQuizAttempt() ->where('type_id', $payload['quiz_id']) ->where('user_id', $payload['user_id']) ->first(); } if ($payload['user_id'] == 0 && $payload['user_token'] != '') { $quiz_attempt = acadlix()->model()->userActivityMeta()->ofQuiz() ->ofQuizAttempt() ->where('type_id', $payload['quiz_id']) ->where('user_token', $payload['user_token']) ->first(); } $custom_logo_id = get_theme_mod('custom_logo'); $res = [ 'logo' => wp_get_attachment_image_url($custom_logo_id, 'full'), 'quiz' => $quiz, 'quiz_attempt' => $quiz_attempt->meta_value ?? 0, ]; // 3) Lifecycle Action: expose retrieval to listeners do_action('acadlix_get_front_quiz_by_id', $res, $quiz, $payload, $params); // 4) Outbound Response Filter: allow response augmentation $res = apply_filters('acadlix_get_front_quiz_by_id_response', $res, $quiz, $payload, $params); return rest_ensure_response($res); } public function post_check_quiz_by_id($request) { // 1) Pre-parameter filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_post_check_quiz_by_id_params', $request->get_json_params(), $request); $res = []; $errors = []; $quiz_id = $params['quiz_id'] ?? $request['quiz_id'] ?? $request->get_param('quiz_id'); if (empty($quiz_id)) { return new WP_Error( 'missing_id', __('Quiz id is required.', 'acadlix'), ['status' => 400] ); } // 2) Payload/Data Filter: normalize values before checking $payload = apply_filters('acadlix_post_check_quiz_by_id_payload', [ 'quiz_id' => intval($quiz_id), 'user_id' => intval($params['user_id'] ?? 0), 'user_token' => $params['user_token'] ?? '', ], $params); $quiz = acadlix()->model()->quiz()->ofQuiz()->find($payload['quiz_id']); // check quiz attempt $per_user_allowed_attempt = $quiz->rendered_metas['quiz_settings']['per_user_allowed_attempt'] ?? 0; if ($per_user_allowed_attempt > 0) { $user_attempts = acadlix()->model()->userActivityMeta()->ofQuiz() ->ofQuizAttempt() ->where('type_id', $payload['quiz_id']) ->when($payload['user_id'] > 0, fn($query) => $query->where('user_id', $payload['user_id'])) ->when($payload['user_id'] == 0 && $payload['user_token'] != '', fn($query) => $query->where('user_token', $payload['user_token'])) ->first(); if ($user_attempts && $user_attempts->meta_value >= $per_user_allowed_attempt) { $errors[] = __('You have reached your maximum limit of attempts.', 'acadlix'); } } // Handle error markup $html = ''; if (count($errors) > 0) { $html .= "
    "; foreach ($errors as $error) { $html .= "
  • {$error}
  • "; } $html .= "
"; } $res['errors'] = $html; // 3) Lifecycle Action: allow listeners to react to check result do_action('acadlix_post_check_quiz_by_id', $res, $quiz, $payload, $params); // 4) Outbound Response Filter: allow other modules to modify the response $res = apply_filters('acadlix_post_check_quiz_by_id_response', $res, $quiz, $payload, $params); return rest_ensure_response($res); } public function post_save_quiz_attempt_by_id($request) { // 1) Pre-parameter filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_post_save_quiz_attempt_by_id_params', $request->get_json_params(), $request); $res = []; $quiz_id = $params['quiz_id'] ?? $request['quiz_id'] ?? $request->get_param('quiz_id'); if (empty($quiz_id)) { return new WP_Error( 'missing_id', __('Quiz id is required.', 'acadlix'), ['status' => 400] ); } // 2) Payload/Data Filter: normalize values before core execution $payload = apply_filters('acadlix_post_save_quiz_attempt_by_id_payload', [ 'quiz_id' => intval($quiz_id), 'user_id' => intval($params['user_id'] ?? 0), 'user_token' => $params['user_token'] ?? '', 'quiz_attempt_type' => $params['quiz_attempt_type'] ?? 'shortcode', 'course_statistic_id' => intval($params['course_statistic_id'] ?? 0), ], $params); $attempts = acadlix()->model()->userActivityMeta()->ofQuiz() ->ofQuizAttempt() ->where('type_id', $payload['quiz_id']) ->when($payload['user_id'] > 0, fn($query) => $query->where('user_id', $payload['user_id'])) ->when($payload['user_id'] == 0 && $payload['user_token'] !== '', fn($query) => $query->where('user_token', $payload['user_token'])) ->first(); if ($attempts) { $attempts->update([ 'meta_value' => (int) $attempts->meta_value + 1, // phpcs:ignore ]); do_action('acadlix_post_save_quiz_attempt_by_id_updated', $attempts, $payload, $params); } else { $attempts = acadlix()->model()->userActivityMeta()->create([ 'user_token' => $payload['user_token'], 'user_id' => $payload['user_id'], 'type' => 'quiz', 'type_id' => $payload['quiz_id'], 'meta_key' => 'quiz_attempt', // phpcs:ignore 'meta_value' => 1, // phpcs:ignore ]); do_action('acadlix_post_save_quiz_attempt_by_id_created', $attempts, $payload, $params); } if ($payload['quiz_attempt_type'] === 'course_statistic' && $payload['course_statistic_id']) { acadlix()->model()->userActivityMeta() ->create([ 'user_token' => $payload['user_token'], 'user_id' => $payload['user_id'], 'type' => $payload['quiz_attempt_type'], 'type_id' => $payload['course_statistic_id'], 'meta_key' => 'course_statistic_quiz_attempt_time', // phpcs:ignore 'meta_value' => time(), // phpcs:ignore ]); } $res['attempts'] = $attempts; // 3) Lifecycle Action: allow listeners to react to save attempt do_action('acadlix_post_save_quiz_attempt_by_id', $res, $attempts, $payload, $params); // 4) Outbound Response Filter: allow other modules to modify the response $res = apply_filters('acadlix_post_save_quiz_attempt_by_id_response', $res, $attempts, $payload, $params); return rest_ensure_response($res); } public function post_save_result_by_id($request) { // 1) Pre-parameter filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_post_save_result_by_id_params', $request->get_json_params(), $request); $quiz_id = $params['quiz_id'] ?? $request['quiz_id'] ?? $request->get_param('quiz_id'); if (empty($quiz_id)) { return new WP_Error( 'missing_id', __('Quiz id is required.', 'acadlix'), ['status' => 400] ); } $quiz = acadlix()->model()->quiz()->ofQuiz()->find($quiz_id); if (!$quiz) { return new WP_Error( 'invalid_quiz', __('Quiz not found.', 'acadlix'), ['status' => 404] ); } // Remote IP extraction securely $remote_addr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : ''; $ip = filter_var($remote_addr, FILTER_VALIDATE_IP) ?: ''; // 2) Payload/Data Filter: normalize values before core execution $payload = apply_filters('acadlix_post_save_result_by_id_payload', [ 'quiz_id' => intval($quiz_id), 'user_id' => intval($params['user_id'] ?? 0), 'user_token' => $params['user_token'] ?? '', 'quiz_attempt_type' => $params['quiz_attempt_type'] ?? 'shortcode', 'course_statistic_id' => intval($params['course_statistic_id'] ?? 0), 'points' => (float) ($params['points'] ?? 0), 'result' => (float) ($params['result'] ?? 0), 'quiz_time' => intval($params['time_taken'] ?? 0), 'accuracy' => (float) ($params['accuracy'] ?? 0), 'status' => $params['status'] ?? '', 'ip' => $ip, ], $params); // Standardized data matrix $data = [ 'quiz_id' => $payload['quiz_id'], 'user_token' => $payload['user_token'], 'user_id' => $payload['user_id'], 'points' => $payload['points'], 'result' => $payload['result'], 'quiz_time' => $payload['quiz_time'], 'accuracy' => $payload['accuracy'], 'status' => $payload['status'], ]; $res = []; $settings = $quiz->rendered_metas['quiz_settings'] ?? []; $stat_ref = null; $top = null; // Handle Statistics Saving $save_statistic = $settings['save_statistic'] ?? false; $statistic_ip_lock = $settings['statistic_ip_lock'] ?? 0; $save_statistic_number_of_times = $settings['save_statistic_number_of_times'] ?? 0; if ($save_statistic) { $statistic_ref = acadlix()->model()->statisticRef()->where('quiz_id', $payload['quiz_id']) ->when($payload['user_id'] > 0, fn($query) => $query->where('user_id', $payload['user_id'])) ->when($payload['user_id'] == 0 && $payload['user_token'] !== '', fn($query) => $query->where('user_token', $payload['user_token'])); $latest_stat = $statistic_ref->latest()->first(); $check_ip_lock = $statistic_ip_lock == 0 || !$latest_stat || round((time() - strtotime($latest_stat->created_at)) / 60) > $statistic_ip_lock; $check_multiple_entry = $save_statistic_number_of_times == 0 || $save_statistic_number_of_times > $statistic_ref->count(); if ($statistic_ref->count() == 0 || ($check_ip_lock && $check_multiple_entry)) { $stat_ref = acadlix()->model()->statisticRef()->create($data); if (!empty($params['questions']) && is_array($params['questions'])) { foreach ($params['questions'] as $question) { $q_res = $question['result'] ?? []; $stat_ref?->statistics()?->create([ 'question_id' => $question['question_id'], 'correct_count' => $q_res['correct_count'] ?? 0, 'partial_correct_count' => $q_res['partial_correct_count'] ?? 0, 'incorrect_count' => $q_res['incorrect_count'] ?? 0, 'hint_count' => $q_res['hint_count'] ?? 0, 'solved_count' => $q_res['solved_count'] ?? 0, 'points' => $q_res['points'] ?? 0, 'negative_points' => $q_res['negative_points'] ?? 0, 'question_time' => $q_res['time'] ?? 0, 'answer_data' => $q_res['answer_data'] ?? '', 'attempted_at' => !empty($q_res['attempted_at']) ? (int) $q_res['attempted_at'] : null, 'is_evaluated' => $q_res['is_evaluated'] ?? 0, 'evaluated_by' => $q_res['evaluated_by'] ?? null, 'evaluated_id' => $q_res['evaluated_id'] ?? null, 'evaluation_remark' => $q_res['evaluation_remark'] ?? '', ]); } } // Save course stats correlation if ($payload['quiz_attempt_type'] === 'course_statistic' && $payload['course_statistic_id'] && $stat_ref) { acadlix()->model()->userActivityMeta()->create([ 'user_token' => $payload['user_token'], 'user_id' => $payload['user_id'], 'type' => $payload['quiz_attempt_type'], 'type_id' => $payload['course_statistic_id'], 'meta_key' => 'statistic_ref_id', 'meta_value' => $stat_ref->id, ]); } do_action('acadlix_post_save_result_by_id_statistic_saved', $stat_ref, $payload, $params, $quiz); } $show_average_score = $settings['show_average_score'] ?? false; $global_stat_ref = acadlix()->model()->statisticRef()->where('quiz_id', $payload['quiz_id']); $res['average_score'] = $global_stat_ref->count() > 0 && $show_average_score ? (float) $global_stat_ref->avg('points') : 0; } // Handle Leaderboard / Toplist $leaderboard = $settings['leaderboard'] ?? false; $leaderboard_user_can_apply_multiple_times = $settings['leaderboard_user_can_apply_multiple_times'] ?? false; $leaderboard_apply_multiple_number_of_times = $settings['leaderboard_apply_multiple_number_of_times'] ?? 0; if ($leaderboard) { $toplist_count = acadlix()->model()->toplist()->where('quiz_id', $payload['quiz_id']) ->when($payload['user_id'] > 0, fn($query) => $query->where('user_id', $payload['user_id'])) ->when($payload['user_id'] == 0 && $payload['user_token'] !== '', fn($query) => $query->where('user_token', $payload['user_token'])) ->count(); $check_multiple_leaderboard_entry = $leaderboard_user_can_apply_multiple_times && ($leaderboard_apply_multiple_number_of_times == 0 || $leaderboard_apply_multiple_number_of_times > $toplist_count); if ($toplist_count == 0 || $check_multiple_leaderboard_entry) { $top = acadlix()->model()->toplist()->create([ ...$data, 'name' => $params['name'] ?? '', 'email' => $params['email'] ?? '', 'ip' => $payload['ip'], ]); do_action('acadlix_post_save_result_by_id_toplist_created', $top, $payload, $params, $quiz); } $res['toplist_id'] = $top?->id ?? 0; $toplist = acadlix()->model()->toplist(); $entries_limit = $settings['leaderboard_total_number_of_entries'] ?? 10; $limit = ($entries_limit > 0 && $entries_limit < 10) ? $entries_limit : 10; $res['toplist'] = $toplist->getTopList($payload['quiz_id'], 0, $limit); $res['toplist_count'] = $toplist->where('quiz_id', $payload['quiz_id'])->count(); } // Handle Email Transmissions if ($payload['user_id'] > 0) { $email_placeholders = apply_filters('acadlix_post_save_result_by_id_email_placeholders', [ '$userId' => $payload['user_id'], '$username' => $params['name'] ?? __('Anonymous', 'acadlix'), '$quizname' => $quiz->post_title ?? '', '$result' => number_format($payload['result'], 2) . '%', '$points' => $payload['points'], '$ip' => $payload['ip'], '$categories' => '', '$subjects' => '', ], $payload, $params, $quiz); // Admin Email if ($settings['admin_email_notification'] ?? false) { $admin_msg = str_replace(array_keys($email_placeholders), $email_placeholders, $settings['admin_message'] ?? ''); acadlix()->helper()->email()->sendEmail( $settings['admin_to'] ?? '', $settings['admin_subject'] ?? '', $admin_msg, $settings['admin_from'] ?? '' ); } // Student Email if (($settings['student_email_notification'] ?? false) && !empty($params['email'])) { $student_msg = str_replace(array_keys($email_placeholders), $email_placeholders, $settings['student_message'] ?? ''); acadlix()->helper()->email()->sendEmail( $params['email'], $settings['student_subject'] ?? '', $student_msg, $settings['student_from'] ?? '' ); } do_action('acadlix_post_save_result_by_id_emails_sent', $payload, $params, $quiz); } // 3) Lifecycle Action: allow listeners to react to result save do_action('acadlix_post_save_result_by_id', $res, $payload, $params, $quiz); // 4) Outbound Response Filter: allow other modules to modify the response $res = apply_filters('acadlix_post_save_result_by_id_response', $res, $payload, $params, $quiz); return rest_ensure_response($res); } public function get_front_quiz_leaderboard_by_id($request) { // 1) Pre-parameter filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_get_front_quiz_leaderboard_by_id_params', $request->get_params(), $request); $quiz_id = $params['quiz_id'] ?? $request['quiz_id'] ?? $request->get_param('quiz_id'); // Validate required fields if (empty($quiz_id)) { return new WP_Error( 'missing_id', __('Quiz id is required.', 'acadlix'), ['status' => 400] ); } // 2) Payload/Data Filter: normalize values before core execution $payload = apply_filters('acadlix_get_front_quiz_leaderboard_by_id_payload', [ 'quiz_id' => intval($quiz_id), ], $params); $appends = apply_filters('acadlix_get_front_quiz_leaderboard_by_id_quiz_appends', [ 'rendered_post_content', 'rendered_metas', 'category', 'languages', ], $payload['quiz_id'], $params); $quiz = acadlix()->model()->quiz()->ofQuiz()->find($payload['quiz_id'])->setAppends($appends); $res = ['quiz' => $quiz]; $leaderboard = $quiz->rendered_metas['quiz_settings']['leaderboard'] ?? false; if ($leaderboard) { $leaderboard_total_number_of_entries = $quiz->rendered_metas['quiz_settings']['leaderboard_total_number_of_entries'] ?? 10; $toplist = acadlix()->model()->toplist(); $limit = ($leaderboard_total_number_of_entries > 0 && $leaderboard_total_number_of_entries < 10) ? $leaderboard_total_number_of_entries : 10; $toplist_data = apply_filters('acadlix_get_front_quiz_leaderboard_by_id_toplist_data', $toplist->getTopList($payload['quiz_id'], 0, $limit), $payload, $params, $quiz); $res['toplist'] = $toplist_data; $res['toplist_count'] = $toplist->where('quiz_id', $payload['quiz_id'])->count(); } // 3) Lifecycle Action: expose retrieval to listeners do_action('acadlix_get_front_quiz_leaderboard_by_id', $res, $quiz, $payload, $params); // 4) Outbound Response Filter: allow response augmentation $res = apply_filters('acadlix_get_front_quiz_leaderboard_by_id_response', $res, $quiz, $payload, $params); return rest_ensure_response($res); } public function post_load_more_toplist_by_id($request) { // 1) Pre-parameter filter: allow callers to mutate incoming params before validation $params = apply_filters('acadlix_pre_post_load_more_toplist_by_id_params', $request->get_json_params(), $request); $quiz_id = $params['quiz_id'] ?? $request['quiz_id'] ?? $request->get_param('quiz_id'); if (empty($quiz_id)) { return new WP_Error( 'missing_id', __('Quiz id is required.', 'acadlix'), ['status' => 400] ); } // 2) Payload/Data Filter: normalize values before core execution $payload = apply_filters('acadlix_post_load_more_toplist_by_id_payload', [ 'quiz_id' => intval($quiz_id), 'leaderboard_total_number_of_entries' => intval($params['leaderboard_total_number_of_entries'] ?? 0), 'toplist_view_count' => intval($params['toplist_view_count'] ?? 0), ], $params); $toplist = acadlix()->model()->toplist(); $remaining = $payload['leaderboard_total_number_of_entries'] - $payload['toplist_view_count']; $limit = ($remaining < 10) ? $remaining : 10; $toplist_data = apply_filters('acadlix_post_load_more_toplist_by_id_toplist_data', $toplist->getTopList($payload['quiz_id'], $payload['toplist_view_count'], $limit), $payload, $params); $res = [ 'toplist' => $toplist_data, 'toplist_count' => $toplist->where('quiz_id', $payload['quiz_id'])->count(), ]; // 3) Lifecycle Action: allow listeners to react to load more do_action('acadlix_post_load_more_toplist_by_id', $res, $payload, $params); // 4) Outbound Response Filter: allow other modules to modify the response $res = apply_filters('acadlix_post_load_more_toplist_by_id_response', $res, $payload, $params); return rest_ensure_response($res); } public function check_permission($request) { return wp_verify_nonce( $request->get_header('X-WP-Nonce'), 'wp_rest' ); } }