PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.1.9
Tutor LMS – eLearning and online course solution v2.1.9
3.9.14 3.9.13 3.9.12 3.9.11 trunk 1.0.0 1.0.0-alpha 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9 1.1.0 1.1.1 1.2.0 1.2.1 1.2.11 1.2.12 1.2.13 1.2.20 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.4.5 1.4.6 1.4.7 1.4.8 1.4.9 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.7.3 1.7.4 1.7.5 1.7.6 1.7.7 1.7.8 1.7.9 1.8.0 1.8.1 1.8.10 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.8.7 1.8.8 1.8.9 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.15 1.9.16 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.1.1 2.1.10 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.3.0 2.4.0 2.5.0 2.6.0 2.6.1 2.6.2 2.7.0 2.7.1 2.7.2 2.7.3 2.7.4 2.7.5 2.7.6 2.7.7 3.0.0 3.0.1 3.0.2 3.1.0 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.4.0 3.4.1 3.4.2 3.5.0 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.7.0 3.7.1 3.7.2 3.7.3 3.7.4 3.8.0 3.8.1 3.8.2 3.8.3 3.9.0 3.9.1 3.9.10 3.9.2 3.9.3 3.9.4 3.9.5 3.9.6 3.9.7 3.9.8 3.9.9
tutor / classes / Quiz.php
tutor / classes Last commit date
Addons.php 3 years ago Admin.php 3 years ago Ajax.php 3 years ago Announcements.php 3 years ago Assets.php 3 years ago Backend_Page_Trait.php 3 years ago Course.php 3 years ago Course_Embed.php 3 years ago Course_Filter.php 3 years ago Course_List.php 3 years ago Course_Settings_Tabs.php 3 years ago Course_Widget.php 3 years ago Custom_Validation.php 3 years ago Dashboard.php 3 years ago FormHandler.php 3 years ago Frontend.php 3 years ago Gutenberg.php 3 years ago Input.php 3 years ago Instructor.php 3 years ago Instructors_List.php 3 years ago Lesson.php 3 years ago Options_V2.php 3 years ago Post_types.php 3 years ago Private_Course_Access.php 3 years ago Q_and_A.php 3 years ago Question_Answers_List.php 3 years ago Quiz.php 3 years ago Quiz_Attempts_List.php 3 years ago RestAPI.php 3 years ago Reviews.php 3 years ago Rewrite_Rules.php 3 years ago Shortcode.php 3 years ago Student.php 3 years ago Students_List.php 3 years ago Taxonomies.php 3 years ago Template.php 3 years ago Theme_Compatibility.php 3 years ago Tools.php 3 years ago Tools_V2.php 3 years ago Tutor.php 3 years ago TutorEDD.php 3 years ago Tutor_Base.php 3 years ago Tutor_Setup.php 3 years ago Upgrader.php 3 years ago User.php 3 years ago Utils.php 3 years ago Video_Stream.php 3 years ago Withdraw.php 3 years ago Withdraw_Requests_List.php 3 years ago WooCommerce.php 3 years ago
Quiz.php
1663 lines
1 <?php
2 /**
3 * Quiz class
4 *
5 * @package Tutor\QuestionAnswer
6 * @author Themeum <support@themeum.com>
7 * @link https://themeum.com
8 * @since 1.0.0
9 */
10
11 namespace TUTOR;
12
13 if ( ! defined( 'ABSPATH' ) ) {
14 exit;
15 }
16
17 use Tutor\Helpers\QueryHelper;
18 use Tutor\Models\CourseModel;
19 use Tutor\Models\QuizModel;
20 /**
21 * Manage quiz operations.
22 *
23 * @since 1.0.0
24 */
25 class Quiz {
26
27 /**
28 * Allowed attrs
29 *
30 * @var array
31 */
32 private $allowed_attributes = array(
33 'src' => array(),
34 'style' => array(),
35 'class' => array(),
36 'id' => array(),
37 'href' => array(),
38 'alt' => array(),
39 'title' => array(),
40 'type' => array(),
41 'controls' => array(),
42 'muted' => array(),
43 'loop' => array(),
44 'poster' => array(),
45 'preload' => array(),
46 'autoplay' => array(),
47 'width' => array(),
48 'height' => array(),
49 );
50
51 /**
52 * Allowed HTML tags
53 *
54 * @var array
55 */
56 private $allowed_html = array( 'img', 'b', 'i', 'br', 'a', 'audio', 'video', 'source' );
57
58 /**
59 * Register hooks
60 *
61 * @since 1.0.0
62 *
63 * @return void
64 */
65 public function __construct() {
66 add_action( 'save_post_tutor_quiz', array( $this, 'save_quiz_meta' ) );
67 add_action( 'wp_ajax_remove_quiz_from_post', array( $this, 'remove_quiz_from_post' ) );
68
69 add_action( 'wp_ajax_tutor_quiz_timeout', array( $this, 'tutor_quiz_timeout' ) );
70
71 // User take the quiz.
72 add_action( 'template_redirect', array( $this, 'start_the_quiz' ) );
73 add_action( 'template_redirect', array( $this, 'answering_quiz' ) );
74 add_action( 'template_redirect', array( $this, 'finishing_quiz_attempt' ) );
75
76 add_action( 'wp_ajax_review_quiz_answer', array( $this, 'review_quiz_answer' ) );
77 // Instructor Feedback Action.
78 add_action( 'wp_ajax_tutor_instructor_feedback', array( $this, 'tutor_instructor_feedback' ) );
79
80 /**
81 * New Design Quiz
82 */
83
84 add_action( 'wp_ajax_tutor_quiz_save', array( $this, 'tutor_quiz_save' ) );
85 add_action( 'wp_ajax_tutor_delete_quiz_by_id', array( $this, 'tutor_delete_quiz_by_id' ) );
86 add_action( 'wp_ajax_tutor_load_quiz_builder_modal', array( $this, 'tutor_load_quiz_builder_modal' ), 10, 0 );
87 add_action( 'wp_ajax_tutor_quiz_builder_get_question_form', array( $this, 'tutor_quiz_builder_get_question_form' ) );
88 add_action( 'wp_ajax_tutor_quiz_modal_update_question', array( $this, 'tutor_quiz_modal_update_question' ) );
89 add_action( 'wp_ajax_tutor_quiz_builder_question_delete', array( $this, 'tutor_quiz_builder_question_delete' ) );
90 add_action( 'wp_ajax_tutor_quiz_question_answer_editor', array( $this, 'tutor_quiz_question_answer_editor' ) );
91 add_action( 'wp_ajax_tutor_save_quiz_answer_options', array( $this, 'tutor_save_quiz_answer_options' ), 10, 0 );
92 add_action( 'wp_ajax_tutor_update_quiz_answer_options', array( $this, 'tutor_update_quiz_answer_options' ) );
93 add_action( 'wp_ajax_tutor_quiz_builder_change_type', array( $this, 'tutor_quiz_builder_change_type' ) );
94 add_action( 'wp_ajax_tutor_quiz_builder_delete_answer', array( $this, 'tutor_quiz_builder_delete_answer' ) );
95 add_action( 'wp_ajax_tutor_quiz_question_sorting', array( $this, 'tutor_quiz_question_sorting' ) );
96 add_action( 'wp_ajax_tutor_quiz_answer_sorting', array( $this, 'tutor_quiz_answer_sorting' ) );
97 add_action( 'wp_ajax_tutor_mark_answer_as_correct', array( $this, 'tutor_mark_answer_as_correct' ) );
98
99 /**
100 * Frontend Stuff
101 */
102 add_action( 'wp_ajax_tutor_render_quiz_content', array( $this, 'tutor_render_quiz_content' ) );
103
104 /**
105 * Quiz abandon action
106 *
107 * @since 1.9.6
108 */
109 add_action( 'wp_ajax_tutor_quiz_abandon', array( $this, 'tutor_quiz_abandon' ) );
110
111 $this->prepare_allowed_html();
112
113 /**
114 * Delete quiz attempt
115 *
116 * @since 2.1.0
117 */
118 add_action( 'wp_ajax_tutor_attempt_delete', array( $this, 'attempt_delete' ) );
119 }
120
121 /**
122 * Prepare allowed HTML
123 *
124 * @since 1.0.0
125 *
126 * @return void
127 */
128 private function prepare_allowed_html() {
129
130 $allowed = array();
131
132 foreach ( $this->allowed_html as $tag ) {
133 $allowed[ $tag ] = $this->allowed_attributes;
134 }
135
136 $this->allowed_html = $allowed;
137 }
138
139 /**
140 * Instructor feedback ajax request handler
141 *
142 * @since 1.0.0
143 *
144 * @return void | send json response
145 */
146 public function tutor_instructor_feedback() {
147 tutor_utils()->checking_nonce();
148
149 // Check if user is privileged.
150 if ( ! current_user_can( 'administrator' ) || ! current_user_can( tutor()->instructor_role ) ) {
151 wp_send_json_error( tutor_utils()->error_message() );
152 }
153
154 $attempt_details = self::attempt_details( Input::post( 'attempt_id', 0, Input::TYPE_INT ) );
155 $feedback = Input::post( 'feedback', '', Input::TYPE_KSES_POST );
156 $attempt_info = isset( $attempt_details->attempt_info ) ? $attempt_details->attempt_info : false;
157 if ( $attempt_info ) {
158 //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
159 $unserialized = unserialize( $attempt_details->attempt_info );
160 if ( is_array( $unserialized ) ) {
161 $unserialized['instructor_feedback'] = $feedback;
162
163 do_action( 'tutor_quiz/attempt/submitted/feedback', $attempt_details->attempt_id );
164 //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
165 $update = self::update_attempt_info( $attempt_details->attempt_id, serialize( $unserialized ) );
166 if ( $update ) {
167 wp_send_json_success();
168 } else {
169 wp_send_json_error();
170 }
171 } else {
172 wp_send_json_error( __( 'Invalid quiz info' ) );
173 }
174 }
175 wp_send_json_error();
176 }
177
178 /**
179 * Update quiz meta
180 *
181 * @since 1.0.0
182 *
183 * @param int $post_ID post id.
184 * @return void
185 */
186 public function save_quiz_meta( $post_ID ) {
187 //phpcs:ignore WordPress.Security.NonceVerification.Missing
188 if ( isset( $_POST['quiz_option'] ) ) {
189 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
190 update_post_meta( $post_ID, 'tutor_quiz_option', $quiz_option );
191 }
192 }
193
194 /**
195 * Remove quiz from post
196 *
197 * @since 1.0.0
198 *
199 * @return void
200 */
201 public function remove_quiz_from_post() {
202 tutor_utils()->checking_nonce();
203
204 global $wpdb;
205 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
206
207 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
208 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
209 }
210
211 $wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'ID' => $quiz_id ) );
212 wp_send_json_success();
213 }
214
215 /**
216 * Start Quiz from here...
217 *
218 * @since 1.0.0
219 *
220 * @return void
221 */
222 public function start_the_quiz() {
223 if ( Input::post( 'tutor_action' ) !== 'tutor_start_quiz' ) {
224 return;
225 }
226 // Checking nonce.
227 tutor_utils()->checking_nonce();
228
229 if ( ! is_user_logged_in() ) {
230 // TODO: need to set a view in the next version.
231 die( 'Please sign in to do this operation' );
232 }
233
234 global $wpdb;
235
236 $user_id = get_current_user_id();
237 $user = get_userdata( $user_id );
238
239 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
240
241 $quiz = get_post( $quiz_id );
242 $course = CourseModel::get_course_by_quiz( $quiz_id );
243 if ( empty( $course->ID ) ) {
244 die( 'There is something went wrong with course, please check if quiz attached with a course' );
245 }
246
247 do_action( 'tutor_quiz/start/before', $quiz_id, $user_id );
248
249 $date = date( 'Y-m-d H:i:s', tutor_time() );
250
251 $tutor_quiz_option = (array) maybe_unserialize( get_post_meta( $quiz_id, 'tutor_quiz_option', true ) );
252 $attempts_allowed = tutor_utils()->get_quiz_option( $quiz_id, 'attempts_allowed', 0 );
253
254 $time_limit = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_value' );
255 $time_limit_seconds = 0;
256 $time_type = 'seconds';
257 if ( $time_limit ) {
258 $time_type = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_type' );
259
260 switch ( $time_type ) {
261 case 'seconds':
262 $time_limit_seconds = $time_limit;
263 break;
264 case 'minutes':
265 $time_limit_seconds = $time_limit * 60;
266 break;
267 case 'hours':
268 $time_limit_seconds = $time_limit * 60 * 60;
269 break;
270 case 'days':
271 $time_limit_seconds = $time_limit * 60 * 60 * 24;
272 break;
273 case 'weeks':
274 $time_limit_seconds = $time_limit * 60 * 60 * 24 * 7;
275 break;
276 }
277 }
278
279 $max_question_allowed = tutor_utils()->max_questions_for_take_quiz( $quiz_id );
280 $tutor_quiz_option['time_limit']['time_limit_seconds'] = $time_limit_seconds;
281
282 $attempt_data = array(
283 'course_id' => $course->ID,
284 'quiz_id' => $quiz_id,
285 'user_id' => $user_id,
286 'total_questions' => $max_question_allowed,
287 'total_answered_questions' => 0,
288 'attempt_info' => maybe_serialize( $tutor_quiz_option ),
289 'attempt_status' => 'attempt_started',
290 'attempt_ip' => tutor_utils()->get_ip(),
291 'attempt_started_at' => $date,
292 );
293
294 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_data );
295 $attempt_id = (int) $wpdb->insert_id;
296
297 do_action( 'tutor_quiz/start/after', $quiz_id, $user_id, $attempt_id );
298
299 wp_safe_redirect( get_permalink( $quiz_id ) );
300 die();
301 }
302
303 /**
304 * Answering quiz
305 *
306 * @since 1.0.0
307 *
308 * @return void
309 */
310 public function answering_quiz() {
311
312 if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
313 return;
314 }
315 // submit quiz attempts.
316 self::tutor_quiz_attemp_submit();
317
318 wp_safe_redirect( get_the_permalink() );
319 die();
320 }
321
322 /**
323 * Quiz abandon submission handler
324 *
325 * @since 1.9.6
326 *
327 * @return JSON response
328 */
329 public function tutor_quiz_abandon() {
330 if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
331 return;
332 }
333 // submit quiz attempts.
334 if ( self::tutor_quiz_attemp_submit() ) {
335 wp_send_json_success();
336 } else {
337 wp_send_json_error();
338 }
339 }
340
341 /**
342 * This is a unified method for handling normal quiz submit or abandon submit
343 * It will handle ajax or normal form submit and can be used with different hooks
344 *
345 * @since 1.9.6
346 *
347 * @return true | false
348 */
349 public static function tutor_quiz_attemp_submit() {
350 // Check logged in.
351 if ( ! is_user_logged_in() ) {
352 die( 'Please sign in to do this operation' );
353 }
354
355 // Check nonce.
356 tutor_utils()->checking_nonce();
357
358 // Prepare attempt info.
359 global $wpdb;
360 $user_id = get_current_user_id();
361 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
362 $attempt = tutor_utils()->get_attempt( $attempt_id );
363 $course_id = CourseModel::get_course_by_quiz( $attempt->quiz_id )->ID;
364
365 // Sanitize data by helper method.
366 $attempt_answers = isset( $_POST['attempt'] ) ? tutor_sanitize_data( $_POST['attempt'] ) : false; //phpcs:ignore
367 $attempt_answers = is_array( $attempt_answers ) ? $attempt_answers : array();
368
369 // Check if has access to the attempt.
370 if ( ! $attempt || $user_id != $attempt->user_id ) {
371 die( 'Operation not allowed, attempt not found or permission denied' );
372 }
373
374 // Before ook.
375 do_action( 'tutor_quiz/attempt_analysing/before', $attempt_id );
376
377 // Loop through every single attempt answer
378 // Single quiz can have multiple question. So multiple answer should be saved.
379 foreach ( $attempt_answers as $attempt_id => $attempt_answer ) {
380
381 /**
382 * Get total marks of all question comes
383 */
384 $question_ids = tutor_utils()->avalue_dot( 'quiz_question_ids', $attempt_answer );
385 $question_ids = array_filter(
386 $question_ids,
387 function( $id ) {
388 return (int) $id;
389 }
390 );
391
392 // Calculate and set the total marks in attempt table for this question.
393 if ( is_array( $question_ids ) && count( $question_ids ) ) {
394 $question_ids_string = QueryHelper::prepare_in_clause( $question_ids );
395
396 // Get total marks of the questions from question table.
397 $total_question_marks = $wpdb->get_var(
398 "SELECT SUM(question_mark)
399 FROM {$wpdb->prefix}tutor_quiz_questions
400 WHERE question_id IN({$question_ids_string});
401 "
402 );
403
404 // Set the the total mark in the attempt table for the question.
405 $wpdb->update(
406 $wpdb->prefix . 'tutor_quiz_attempts',
407 array( 'total_marks' => $total_question_marks ),
408 array( 'attempt_id' => $attempt_id )
409 );
410 }
411
412 $total_marks = 0;
413 $review_required = false;
414 $quiz_answers = tutor_utils()->avalue_dot( 'quiz_question', $attempt_answer );
415
416 if ( tutor_utils()->count( $quiz_answers ) ) {
417
418 foreach ( $quiz_answers as $question_id => $answers ) {
419 $question = QuizModel::get_quiz_question_by_id( $question_id );
420 $question_type = $question->question_type;
421
422 $is_answer_was_correct = false;
423 $given_answer = '';
424
425 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
426
427 if ( ! is_numeric( $answers ) || ! $answers ) {
428 wp_send_json_error();
429 exit;
430 }
431
432 $given_answer = $answers;
433 $is_answer_was_correct = (bool) $wpdb->get_var(
434 $wpdb->prepare(
435 "SELECT is_correct
436 FROM {$wpdb->prefix}tutor_quiz_question_answers
437 WHERE answer_id = %d
438 ",
439 $answers
440 )
441 );
442
443 } elseif ( 'multiple_choice' === $question_type ) {
444
445 $given_answer = (array) ( $answers );
446
447 $given_answer = array_filter(
448 $given_answer,
449 function( $id ) {
450 return is_numeric( $id ) && $id > 0;
451 }
452 );
453 $get_original_answers = (array) $wpdb->get_col(
454 $wpdb->prepare(
455 "SELECT
456 answer_id
457 FROM
458 {$wpdb->prefix}tutor_quiz_question_answers
459 WHERE belongs_question_id = %d
460 AND belongs_question_type = %s
461 AND is_correct = 1 ;
462 ",
463 $question->question_id,
464 $question_type
465 )
466 );
467
468 if ( count( array_diff( $get_original_answers, $given_answer ) ) === 0 && count( $get_original_answers ) === count( $given_answer ) ) {
469 $is_answer_was_correct = true;
470 }
471 $given_answer = maybe_serialize( $answers );
472
473 } elseif ( 'fill_in_the_blank' === $question_type ) {
474
475 $get_original_answer = $wpdb->get_row(
476 $wpdb->prepare(
477 "SELECT *
478 FROM {$wpdb->prefix}tutor_quiz_question_answers
479 WHERE belongs_question_id = %d
480 AND belongs_question_type = %s ;
481 ",
482 $question->question_id,
483 $question_type
484 )
485 );
486
487 /**
488 * Answers stored in DB
489 */
490 $gap_answer = (array) explode( '|', $get_original_answer->answer_two_gap_match );
491 $gap_answer = maybe_serialize( array_map( function ( $ans) {
492 return wp_slash( trim( $ans ) );
493 }, $gap_answer ) );
494
495 /**
496 * Answers from user input
497 */
498 $given_answer = (array) array_map( 'sanitize_text_field', $answers );
499 $given_answer = maybe_serialize( $given_answer );
500
501 /**
502 * Compare answer's by making both case-insensitive.
503 */
504 if ( strtolower( $given_answer ) == strtolower( $gap_answer ) ) {
505 $is_answer_was_correct = true;
506 }
507 } elseif ( 'open_ended' === $question_type || 'short_answer' === $question_type ) {
508 $review_required = true;
509 $given_answer = wp_kses_post( $answers );
510
511 } elseif ( 'ordering' === $question_type || 'matching' === $question_type || 'image_matching' === $question_type ) {
512
513 $given_answer = (array) array_map( 'sanitize_text_field', tutor_utils()->avalue_dot( 'answers', $answers ) );
514 $given_answer = maybe_serialize( $given_answer );
515
516 $get_original_answers = (array) $wpdb->get_col(
517 $wpdb->prepare(
518 "SELECT answer_id
519 FROM {$wpdb->prefix}tutor_quiz_question_answers
520 WHERE belongs_question_id = %d
521 AND belongs_question_type = %s
522 ORDER BY answer_order ASC ;
523 ",
524 $question->question_id,
525 $question_type
526 )
527 );
528
529 $get_original_answers = array_map( 'sanitize_text_field', $get_original_answers );
530
531 if ( maybe_serialize( $get_original_answers ) == $given_answer ) {
532 $is_answer_was_correct = true;
533 }
534 } elseif ( 'image_answering' === $question_type ) {
535 $image_inputs = tutor_utils()->avalue_dot( 'answer_id', $answers );
536 $image_inputs = (array) array_map( 'sanitize_text_field', $image_inputs );
537 $given_answer = maybe_serialize( $image_inputs );
538 $is_answer_was_correct = false;
539 /**
540 * For the image_answering question type result
541 * remain pending in spite of correct answer & required
542 * review of admin/instructor. Since it's
543 * pending we need to mark it as incorrect. Otherwise if
544 * mark it correct then earned mark will be updated. then
545 * again when instructor/admin review & mark it as correct
546 * extra mark is adding. In this case, student
547 * getting double mark for the same question.
548 *
549 * For now code is commenting will be removed later on
550 *
551 * @since 2.1.5
552 */
553
554 // $db_answer = $wpdb->get_col(
555 // $wpdb->prepare(
556 // "SELECT answer_title
557 // FROM {$wpdb->prefix}tutor_quiz_question_answers
558 // WHERE belongs_question_id = %d
559 // AND belongs_question_type = 'image_answering'
560 // ORDER BY answer_order asc ;",
561 // $question_id
562 // )
563 // );
564
565 // if ( is_array( $db_answer ) && count( $db_answer ) ) {
566 // $is_answer_was_correct = ( strtolower( maybe_serialize( array_values( $image_inputs ) ) ) == strtolower( maybe_serialize( $db_answer ) ) );
567 // }
568 }
569
570 $question_mark = $is_answer_was_correct ? $question->question_mark : 0;
571 $total_marks += $question_mark;
572
573 $answers_data = array(
574 'user_id' => $user_id,
575 'quiz_id' => $attempt->quiz_id,
576 'question_id' => $question_id,
577 'quiz_attempt_id' => $attempt_id,
578 'given_answer' => $given_answer,
579 'question_mark' => $question->question_mark,
580 'achieved_mark' => $question_mark,
581 'minus_mark' => 0,
582 'is_correct' => $is_answer_was_correct ? 1 : 0,
583 );
584
585 /**
586 * Check if question_type open ended or short ans the set
587 * is_correct default value null before saving
588 */
589 if ( in_array( $question_type, array( 'open_ended', 'short_answer', 'image_answering' ) ) ) {
590 $answers_data['is_correct'] = null;
591 $review_required = true;
592 }
593
594 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answers_data );
595 }
596 }
597
598 $attempt_info = array(
599 'total_answered_questions' => tutor_utils()->count( $quiz_answers ),
600 'earned_marks' => $total_marks,
601 'attempt_status' => 'attempt_ended',
602 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ),
603 );
604
605 if ( $review_required ) {
606 $attempt_info['attempt_status'] = 'review_required';
607 }
608
609 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
610 }
611
612 // After hook.
613 do_action( 'tutor_quiz/attempt_ended', $attempt_id, $course_id, $user_id );
614
615 return true;
616 }
617
618
619 /**
620 * Quiz attempt will be finish here
621 *
622 * @since 1.0.0
623 *
624 * @return void
625 */
626 public function finishing_quiz_attempt() {
627
628 if ( Input::post( 'tutor_action' ) !== 'tutor_finish_quiz_attempt' ) {
629 return;
630 }
631 // Checking nonce.
632 tutor_utils()->checking_nonce();
633
634 if ( ! is_user_logged_in() ) {
635 die( 'Please sign in to do this operation' );
636 }
637
638 global $wpdb;
639
640 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
641 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
642 $attempt_id = $attempt->attempt_id;
643
644 $attempt_info = array(
645 'total_answered_questions' => 0,
646 'earned_marks' => 0,
647 'attempt_status' => 'attempt_ended',
648 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ),
649 );
650
651 do_action( 'tutor_quiz_before_finish', $attempt_id, $quiz_id, $attempt->user_id );
652 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
653 do_action( 'tutor_quiz_finished', $attempt_id, $quiz_id, $attempt->user_id );
654
655 wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
656 }
657
658 /**
659 * Quiz timeout by ajax
660 *
661 * @since 1.0.0
662 *
663 * @return void
664 */
665 public function tutor_quiz_timeout() {
666 tutils()->checking_nonce();
667
668 global $wpdb;
669
670 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
671 $attempt = tutor_utils()->is_started_quiz( $quiz_id );
672
673 if ( $attempt ) {
674 $attempt_id = $attempt->attempt_id;
675
676 $data = array(
677 'attempt_status' => 'attempt_timeout',
678 'attempt_ended_at' => date( 'Y-m-d H:i:s', tutor_time() ),
679 );
680 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $data, array( 'attempt_id' => $attempt->attempt_id ) );
681
682 do_action( 'tutor_quiz_timeout', $attempt_id, $quiz_id, $attempt->user_id );
683
684 wp_send_json_success();
685 }
686
687 wp_send_json_error( __( 'Quiz has been timeout already', 'tutor' ) );
688 }
689
690 /**
691 * Review quiz answer
692 *
693 * @since 1.0.0
694 *
695 * @return void
696 */
697 public function review_quiz_answer() {
698
699 tutor_utils()->checking_nonce();
700
701 global $wpdb;
702
703 $attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
704 $context = Input::post( 'context' );
705 $attempt_answer_id = Input::post( 'attempt_answer_id', 0, Input::TYPE_INT );
706 $mark_as = Input::post( 'mark_as' );
707
708 if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) || ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
709 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
710 }
711
712 $attempt_answer = $wpdb->get_row(
713 $wpdb->prepare(
714 "SELECT *
715 FROM {$wpdb->prefix}tutor_quiz_attempt_answers
716 WHERE attempt_answer_id = %d
717 ",
718 $attempt_answer_id
719 )
720 );
721
722 $attempt = tutor_utils()->get_attempt( $attempt_id );
723 $question = QuizModel::get_quiz_question_by_id( $attempt_answer->question_id );
724 $course_id = $attempt->course_id;
725 $student_id = $attempt->user_id;
726 $previous_ans = $attempt_answer->is_correct;
727
728 do_action( 'tutor_quiz_review_answer_before', $attempt_answer_id, $attempt_id, $mark_as );
729
730 if ( 'correct' === $mark_as ) {
731
732 $answer_update_data = array(
733 'achieved_mark' => $attempt_answer->question_mark,
734 'is_correct' => 1,
735 );
736 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
737 if ( 0 == $previous_ans || null == $previous_ans ) {
738 // if previous answer was wrong or in review then add point as correct.
739 $attempt_update_data = array(
740 'earned_marks' => $attempt->earned_marks + $attempt_answer->question_mark,
741 'is_manually_reviewed' => 1,
742 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),
743 );
744 }
745
746 if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
747 $attempt_update_data['attempt_status'] = 'attempt_ended';
748 }
749 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
750
751 } elseif ( 'incorrect' === $mark_as ) {
752
753 $answer_update_data = array(
754 'achieved_mark' => '0.00',
755 'is_correct' => 0,
756 );
757 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
758
759 if ( 1 == $previous_ans ) {
760 // If previous ans was right then mynus.
761 $attempt_update_data = array(
762 'earned_marks' => $attempt->earned_marks - $attempt_answer->question_mark,
763 'is_manually_reviewed' => 1,
764 'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),
765 );
766 }
767 if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
768 $attempt_update_data['attempt_status'] = 'attempt_ended';
769 }
770
771 $wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
772 }
773 do_action( 'tutor_quiz_review_answer_after', $attempt_answer_id, $attempt_id, $mark_as );
774 do_action( 'tutor_quiz/answer/review/after', $attempt_answer_id, $course_id, $student_id );
775
776 ob_start();
777 tutor_load_template_from_custom_path(
778 tutor()->path . '/views/quiz/attempt-details.php',
779 array(
780 'attempt_id' => $attempt_id,
781 'user_id' => $student_id,
782 'context' => $context,
783 'back_url' => Input::post( 'back_url' ),
784 )
785 );
786 wp_send_json_success( array( 'html' => ob_get_clean() ) );
787 }
788
789 /**
790 * Save single quiz into database and send html response
791 *
792 * @since 1.0.0
793 *
794 * @return void
795 */
796 public function tutor_quiz_save() {
797 tutor_utils()->checking_nonce();
798 // Prepare args.
799 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
800 $ex_quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
801 $quiz_title = Input::post( 'quiz_title' );
802 $quiz_description = isset( $_POST['quiz_description'] ) ? wp_kses( wp_unslash( $_POST['quiz_description'] ), $this->allowed_html ) : ''; //phpcs:ignore
803
804 $next_order_id = tutor_utils()->get_next_course_content_order_id( $topic_id, $ex_quiz_id );
805
806 // Check edit privilege.
807 if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
808 wp_send_json_error(
809 array(
810 'message' => __( 'Access Denied', 'tutor' ),
811 'data' => array(),
812 )
813 );
814 }
815
816 // Prepare quiz data to save in database.
817 $post_arr = array(
818 'ID' => $ex_quiz_id,
819 'post_type' => 'tutor_quiz',
820 'post_title' => $quiz_title,
821 'post_content' => $quiz_description,
822 'post_status' => 'publish',
823 'post_author' => get_current_user_id(),
824 'post_parent' => $topic_id,
825 'menu_order' => $next_order_id,
826 );
827
828 // Insert quiz and run hook.
829 $quiz_id = wp_insert_post( $post_arr );
830 do_action( ( $ex_quiz_id ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
831
832 // Sanitize by helper method & save quiz settings.
833 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
834 update_post_meta( $quiz_id, 'tutor_quiz_option', $quiz_option );
835 do_action( 'tutor_quiz_settings_updated', $quiz_id );
836
837 // Generate quiz modal to show in modal.
838 $output = $this->tutor_load_quiz_builder_modal(
839 array(
840 'topic_id' => $topic_id,
841 'quiz_id' => $quiz_id,
842 ),
843 true
844 );
845
846 // Generate quiz list to show under topic as sub list.
847 ob_start();
848 tutor_load_template_from_custom_path(
849 tutor()->path . '/views/fragments/quiz-list-single.php',
850 array(
851 'quiz_id' => $quiz_id,
852 'topic_id' => $topic_id,
853 'quiz_title' => $quiz_title,
854 ),
855 false
856 );
857 $output_quiz_row = ob_get_clean();
858
859 wp_send_json_success(
860 array(
861 'output' => $output,
862 'output_quiz_row' => $output_quiz_row,
863 )
864 );
865 }
866
867 /**
868 * Delete quiz by id
869 *
870 * @since 1.0.0
871 *
872 * @return void
873 */
874 public function tutor_delete_quiz_by_id() {
875 tutor_utils()->checking_nonce();
876
877 global $wpdb;
878
879 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
880 $post = get_post( $quiz_id );
881
882 if ( ! tutils()->can_user_manage( 'quiz', $quiz_id ) ) {
883 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
884 }
885
886 if ( 'tutor_quiz' === $post->post_type ) {
887 do_action( 'tutor_delete_quiz_before', $quiz_id );
888
889 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
890 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );
891
892 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );
893
894 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
895 $in_question_ids = QueryHelper::prepare_in_clause( $questions_ids );
896 $wpdb->query(
897 "DELETE
898 FROM {$wpdb->prefix}tutor_quiz_question_answers
899 WHERE belongs_question_id IN({$in_question_ids})
900 "
901 );
902 }
903
904 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );
905
906 wp_delete_post( $quiz_id, true );
907
908 do_action( 'tutor_delete_quiz_after', $quiz_id );
909
910 wp_send_json_success();
911 }
912
913 wp_send_json_error();
914 }
915
916 /**
917 * Load quiz Modal on add/edit click
918 *
919 * @since 1.0.0
920 *
921 * @param array $params params.
922 * @param boolean $return should return or not.
923 *
924 * @return mixed
925 */
926 public function tutor_load_quiz_builder_modal( $params = array(), $return = false ) {
927 tutor_utils()->checking_nonce();
928
929 //phpcs:ignore WordPress.Security.NonceVerification.Missing
930 $data = array_merge( $_POST, $params );
931 $quiz_id = isset( $data['quiz_id'] ) ? sanitize_text_field( $data['quiz_id'] ) : 0;
932 $topic_id = isset( $data['topic_id'] ) ? sanitize_text_field( $data['topic_id'] ) : 0;
933 $quiz = $quiz_id ? get_post( $quiz_id ) : null;
934 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
935
936 if ( $quiz_id && ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
937 wp_send_json_error( array( 'message' => __( 'Quiz Permission Denied', 'tutor' ) ) );
938 }
939
940 ob_start();
941 include tutor()->path . 'views/modal/edit_quiz.php';
942 $output = ob_get_clean();
943
944 if ( $return ) {
945 return $output;
946 }
947
948 wp_send_json_success( array( 'output' => $output ) );
949 }
950
951 /**
952 * Load quiz question form for quiz
953 *
954 * @since 1.0.0
955 *
956 * @return void
957 */
958 public function tutor_quiz_builder_get_question_form() {
959 tutor_utils()->checking_nonce();
960
961 global $wpdb;
962 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
963 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
964 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
965
966 // check if the user can manage the quiz.
967 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
968 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
969 }
970
971 // If question ID not provided, then create new before rendering the form.
972 if ( ! $question_id ) {
973 $next_question_id = QuizModel::quiz_next_question_id();
974 $next_question_order = QuizModel::quiz_next_question_order_id( $quiz_id );
975 $question_title = __( 'Question', 'tutor' ) . ' ' . $next_question_id;
976
977 $new_question_data = array(
978 'quiz_id' => $quiz_id,
979 'question_title' => $question_title,
980 'question_description' => '',
981 'question_type' => 'true_false',
982 'question_mark' => 1,
983 'question_settings' => maybe_serialize( array() ),
984 'question_order' => esc_sql( $next_question_order ),
985 );
986
987 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_questions', $new_question_data );
988 $question_id = $wpdb->insert_id;
989
990 // Add default true/false options for this question since it is by default true/false type.
991 $question_array = array(
992 $question_id => array(
993 'Question' => $question_title,
994 'question_type' => 'true_false',
995 'question_mark' => '1.00',
996 'question_description' => '',
997 ),
998 );
999
1000 $answer_array = array(
1001 $question_id => array(
1002 'true_false' => true,
1003 ),
1004 );
1005
1006 $this->tutor_save_quiz_answer_options( $question_array, $answer_array, false );
1007 }
1008
1009 // Now get all data by this question id.
1010 $question = $wpdb->get_row(
1011 $wpdb->prepare(
1012 "SELECT * FROM {$wpdb->prefix}tutor_quiz_questions
1013 WHERE question_id = %d ",
1014 $question_id
1015 )
1016 );
1017
1018 // Render the question form finally.
1019 ob_start();
1020 require tutor()->path . 'views/modal/question_form.php';
1021 $output = ob_get_clean();
1022
1023 wp_send_json_success( array( 'output' => $output ) );
1024 }
1025
1026 /**
1027 * Update quiz modal
1028 *
1029 * @since 1.0.0
1030 *
1031 * @return void
1032 */
1033 public function tutor_quiz_modal_update_question() {
1034 tutor_utils()->checking_nonce();
1035
1036 global $wpdb;
1037 // Sanitize $_POST below before using.
1038 $quiz_question_id = Input::post( 'tutor_quiz_question_id', 0, Input::TYPE_INT );
1039 if ( ! $quiz_question_id ) {
1040 wp_send_json_error( __( 'Invalid quiz question ID', 'tutor' ) );
1041 }
1042
1043 /**
1044 * Sanitize $_POST[tutor_quiz_question] data through array_walk
1045 * it will override & sanitize all the question data.
1046 *
1047 * @since 2.1.3
1048 */
1049 if ( isset( $_POST['tutor_quiz_question'][ $quiz_question_id ] ) ) {
1050 array_walk(
1051 $_POST['tutor_quiz_question'][ $quiz_question_id ], // phpcs:ignore
1052 function( $v, $k ) use ( $quiz_question_id ) {
1053 if ( 'question_description' === $k ) {
1054 $_POST['tutor_quiz_question'][ $quiz_question_id ][ $k ] = wp_kses_post( wp_unslash( $v ) );
1055 } else {
1056 $_POST['tutor_quiz_question'][ $quiz_question_id ][ $k ] = sanitize_text_field( wp_unslash( $v ) );
1057 }
1058 }
1059 );
1060 } else {
1061 wp_send_json_error( __( 'Invalid quiz question ID', 'tutor' ) );
1062 }
1063
1064 $question_data = wp_unslash( $_POST['tutor_quiz_question'] ); //phpcs:ignore
1065 $requires_answeres = array(
1066 'multiple_choice',
1067 'single_choice',
1068 'true_false',
1069 'fill_in_the_blank',
1070 'matching',
1071 'image_matching',
1072 'image_answering',
1073 'ordering',
1074 );
1075
1076 $need_correct = array(
1077 'multiple_choice',
1078 'single_choice',
1079 'true_false',
1080 );
1081
1082 foreach ( $question_data as $question_id => $question ) {
1083 // Make sure the quiz has answers.
1084 if ( isset( $question['question_type'] ) && in_array( $question['question_type'], $requires_answeres ) ) {
1085 $require_correct = in_array( $question['question_type'], $need_correct );
1086 $all_answers = $this->get_answers_by_q_id( $question_id, $question['question_type'] );
1087 $correct_answers = $this->get_answers_by_q_id( $question_id, $question['question_type'], $require_correct );
1088
1089 if ( ! empty( $all_answers ) && empty( $correct_answers ) ) {
1090 wp_send_json_error( array( 'message' => __( 'Please make sure the question has answer' ) ) );
1091 exit;
1092 }
1093 }
1094
1095 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1096 continue;
1097 }
1098 // Data already sanitize above.
1099 $question_title = $question['question_title'] ?? '';
1100 $question_description = $question['question_description'];
1101 $question_type = $question['question_type'] ?? '';
1102 $question_mark = $question['question_mark'] ?? '';
1103
1104 unset( $question['question_title'] );
1105 unset( $question['question_description'] );
1106
1107 $data = array(
1108 'question_title' => $question_title,
1109 'question_description' => $question_description,
1110 'question_type' => $question_type,
1111 'question_mark' => $question_mark,
1112 'question_settings' => maybe_serialize( $question ),
1113 );
1114
1115 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );
1116 }
1117
1118 wp_send_json_success();
1119 }
1120
1121 /**
1122 * Delete quiz questions
1123 *
1124 * @since 1.0.0
1125 *
1126 * @return void
1127 */
1128 public function tutor_quiz_builder_question_delete() {
1129 tutor_utils()->checking_nonce();
1130
1131 global $wpdb;
1132
1133 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1134
1135 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1136 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1137 }
1138
1139 if ( $question_id ) {
1140 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => esc_sql( $question_id ) ) );
1141 }
1142
1143 wp_send_json_success();
1144 }
1145
1146 /**
1147 * Get answers options form for quiz question
1148 *
1149 * @since 1.0.0
1150 *
1151 * @return void send wp_json response
1152 */
1153 public function tutor_quiz_question_answer_editor() {
1154 tutor_utils()->checking_nonce();
1155
1156 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1157 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1158 $quiz_option = isset( $_POST['tutor_quiz_question'] ) ? tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ) : array(); //phpcs:ignore
1159 $question = tutor_utils()->avalue_dot( $question_id, $quiz_option );
1160 $question_type = $question['question_type'];
1161
1162 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1163 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1164 }
1165
1166 if ( $answer_id ) {
1167 $old_answer = tutor_utils()->get_answer_by_id( $answer_id );
1168 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
1169 foreach ( $old_answer as $old_answer ) {
1170 }
1171 }
1172
1173 ob_start();
1174 include tutor()->path . 'views/modal/question_answer_form.php';
1175 $output = ob_get_clean();
1176
1177 wp_send_json_success( array( 'output' => $output ) );
1178 }
1179
1180 /**
1181 * Undocumented function
1182 *
1183 * @since 1.0.0
1184 *
1185 * @param mixed $questions questions.
1186 * @param mixed $answers answers.
1187 * @param boolean $response should send json response.
1188 *
1189 * @return void
1190 */
1191 public function tutor_save_quiz_answer_options( $questions = null, $answers = null, $response = true ) {
1192 tutor_utils()->checking_nonce();
1193
1194 global $wpdb;
1195 $questions = $questions ? $questions : tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1196 $answers = $answers ? $answers : tutor_utils()->sanitize_array( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1197
1198 foreach ( $answers as $question_id => $answer ) {
1199 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1200 continue;
1201 }
1202
1203 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1204 $question_type = $question['question_type'];
1205
1206 // Getting next sorting order.
1207 $next_order_id = (int) $wpdb->get_var(
1208 $wpdb->prepare(
1209 "SELECT MAX(answer_order)
1210 FROM {$wpdb->prefix}tutor_quiz_question_answers
1211 WHERE belongs_question_id = %d
1212 AND belongs_question_type = %s
1213 ",
1214 $question_id,
1215 esc_sql( $question_type )
1216 )
1217 );
1218
1219 //phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1220 $next_order_id = $next_order_id + 1;
1221
1222 if ( $question ) {
1223 if ( 'true_false' === $question_type ) {
1224 $wpdb->delete(
1225 $wpdb->prefix . 'tutor_quiz_question_answers',
1226 array(
1227 'belongs_question_id' => $question_id,
1228 'belongs_question_type' => $question_type,
1229 )
1230 );
1231 $data_true_false = array(
1232 array(
1233 'belongs_question_id' => esc_sql( $question_id ),
1234 'belongs_question_type' => $question_type,
1235 'answer_title' => __( 'True', 'tutor' ),
1236 'is_correct' => 'true' == $answer['true_false'] ? 1 : 0,
1237 'answer_two_gap_match' => 'true',
1238 ),
1239 array(
1240 'belongs_question_id' => esc_sql( $question_id ),
1241 'belongs_question_type' => $question_type,
1242 'answer_title' => __( 'False', 'tutor' ),
1243 'is_correct' => 'false' === $answer['true_false'] ? 1 : 0,
1244 'answer_two_gap_match' => 'false',
1245 ),
1246 );
1247
1248 foreach ( $data_true_false as $true_false_data ) {
1249 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
1250 }
1251 } elseif ( 'multiple_choice' === $question_type ||
1252 'single_choice' === $question_type ||
1253 'ordering' === $question_type ||
1254 'matching' === $question_type ||
1255 'image_matching' === $question_type ||
1256 'image_answering' === $question_type ) {
1257
1258 $answer_data = array(
1259 'belongs_question_id' => sanitize_text_field( $question_id ),
1260 'belongs_question_type' => $question_type,
1261 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1262 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1263 'answer_view_format' => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
1264 'answer_order' => $next_order_id,
1265 );
1266 if ( isset( $answer['matched_answer_title'] ) ) {
1267 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1268 }
1269
1270 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1271
1272 } elseif ( 'fill_in_the_blank' === $question_type ) {
1273 $wpdb->delete(
1274 $wpdb->prefix . 'tutor_quiz_question_answers',
1275 array(
1276 'belongs_question_id' => $question_id,
1277 'belongs_question_type' => $question_type,
1278 )
1279 );
1280 $answer_data = array(
1281 'belongs_question_id' => sanitize_text_field( $question_id ),
1282 'belongs_question_type' => $question_type,
1283 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1284 'answer_two_gap_match' => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
1285 );
1286 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1287 }
1288 }
1289 }
1290
1291 // Send response to browser if not internal call.
1292 if ( $response ) {
1293 wp_send_json_success();
1294 exit;
1295 }
1296 }
1297
1298 /**
1299 * Tutor Update Answer
1300 *
1301 * @since 1.0.0
1302 *
1303 * @return void send wp_json response
1304 */
1305 public function tutor_update_quiz_answer_options() {
1306 tutor_utils()->checking_nonce();
1307
1308 global $wpdb;
1309
1310 $answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );
1311
1312 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1313 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1314 }
1315
1316 // Data sanitizing by helper method.
1317 $questions = tutor_sanitize_data( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1318 $answers = tutor_sanitize_data( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1319
1320 foreach ( $answers as $question_id => $answer ) {
1321 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1322 $question_type = $question['question_type'];
1323
1324 if ( $question ) {
1325 if ( 'multiple_choice' === $question_type ||
1326 'single_choice' === $question_type ||
1327 'ordering' === $question_type ||
1328 'matching' === $question_type ||
1329 'image_matching' === $question_type ||
1330 'fill_in_the_blank' === $question_type ||
1331 'image_answering' === $question_type ) {
1332
1333 $answer_data = array(
1334 'belongs_question_id' => $question_id,
1335 'belongs_question_type' => $question_type,
1336 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1337 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1338 'answer_view_format' => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
1339 );
1340 if ( isset( $answer['matched_answer_title'] ) ) {
1341 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1342 }
1343
1344 if ( 'fill_in_the_blank' === $question_type ) {
1345 $answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
1346 }
1347
1348 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
1349 }
1350 }
1351 }
1352 wp_send_json_success();
1353 }
1354
1355 /**
1356 * Get answers by quiz id
1357 *
1358 * @since 1.0.0
1359 *
1360 * @param int $question_id question id.
1361 * @param mixed $question_type type of question.
1362 * @param boolean $is_correct only correct answers or not.
1363 *
1364 * @return wpdb:get_results
1365 */
1366 private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
1367 global $wpdb;
1368
1369 $correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1370
1371 return $wpdb->get_results(
1372 $wpdb->prepare(
1373 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1374 WHERE belongs_question_id = %d
1375 AND belongs_question_type = %s
1376 {$correct_clause}
1377 ORDER BY answer_order ASC;
1378 ",
1379 $question_id,
1380 esc_sql( $question_type )
1381 )
1382 );
1383 }
1384
1385 /**
1386 * Quiz builder changed type
1387 *
1388 * @since 1.0.0
1389 *
1390 * @return void send wp_json response
1391 */
1392 public function tutor_quiz_builder_change_type() {
1393 tutor_utils()->checking_nonce();
1394
1395 global $wpdb;
1396 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1397 $question_type = Input::post( 'question_type' );
1398
1399 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1400 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1401 }
1402
1403 // Get question data by question ID.
1404 $question = $wpdb->get_row(
1405 $wpdb->prepare(
1406 "SELECT *
1407 FROM {$wpdb->prefix}tutor_quiz_questions
1408 WHERE question_id = %d
1409 ",
1410 $question_id
1411 )
1412 );
1413
1414 // Get answers by question ID.
1415 $answers = $this->get_answers_by_q_id( $question_id, $question_type );
1416
1417 ob_start();
1418 require tutor()->path . '/views/modal/question_answer_list.php';
1419 $output = ob_get_clean();
1420
1421 wp_send_json_success( array( 'output' => $output ) );
1422 }
1423
1424 /**
1425 * Delete quiz question's answer
1426 *
1427 * @since 1.0.0
1428 *
1429 * @return void send wp_json response
1430 */
1431 public function tutor_quiz_builder_delete_answer() {
1432 tutor_utils()->checking_nonce();
1433
1434 global $wpdb;
1435 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1436
1437 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1438 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1439 }
1440
1441 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => esc_sql( $answer_id ) ) );
1442 wp_send_json_success();
1443 }
1444
1445 /**
1446 * Save quiz questions sorting
1447 *
1448 * @since 1.0.0
1449 *
1450 * @return void
1451 */
1452 public function tutor_quiz_question_sorting() {
1453 tutor_utils()->checking_nonce();
1454
1455 global $wpdb;
1456
1457 // Data sanitizing by helper method.
1458 $question_ids = tutor_utils()->avalue_dot( 'sorted_question_ids', tutor_sanitize_data( $_POST ) ); //phpcs:ignore
1459 if ( is_array( $question_ids ) && count( $question_ids ) ) {
1460 $i = 0;
1461 foreach ( $question_ids as $key => $question_id ) {
1462 if ( tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1463 $i++;
1464 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_order' => $i ), array( 'question_id' => $question_id ) );
1465 }
1466 }
1467 }
1468 }
1469
1470 /**
1471 * Save sorting data for quiz answers
1472 *
1473 * @since 1.0.0
1474 *
1475 * @return void
1476 */
1477 public function tutor_quiz_answer_sorting() {
1478 tutor_utils()->checking_nonce();
1479
1480 global $wpdb;
1481 $answer_ids = Input::post( 'sorted_answer_ids', array(), Input::TYPE_ARRAY );
1482 if ( count( $answer_ids ) ) {
1483 $i = 0;
1484 foreach ( $answer_ids as $key => $answer_id ) {
1485 if ( tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1486 $i++;
1487 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_order' => $i ), array( 'answer_id' => $answer_id ) );
1488 }
1489 }
1490 }
1491 }
1492
1493 /**
1494 * Mark answer as correct
1495 *
1496 * @since 1.0.0
1497 *
1498 * @return void
1499 */
1500 public function tutor_mark_answer_as_correct() {
1501 tutor_utils()->checking_nonce();
1502
1503 global $wpdb;
1504
1505 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1506 // get question info.
1507 $belong_question = $wpdb->get_row(
1508 $wpdb->prepare(
1509 " SELECT belongs_question_id, belongs_question_type
1510 FROM {$wpdb->tutor_quiz_question_answers}
1511 WHERE answer_id = %d
1512 LIMIT 1
1513 ",
1514 $answer_id
1515 )
1516 );
1517 if ( $belong_question ) {
1518 // if question found update all answer is_correct to 0 except post answer.
1519 $question_type = $belong_question->belongs_question_type;
1520 $question_id = $belong_question->belongs_question_id;
1521 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
1522 $update = $wpdb->query(
1523 $wpdb->prepare(
1524 "UPDATE {$wpdb->tutor_quiz_question_answers}
1525 SET is_correct = 0
1526 WHERE belongs_question_id = %d
1527 AND answer_id != %d
1528 ",
1529 $question_id,
1530 $answer_id
1531 )
1532 );
1533 }
1534 }
1535
1536 $input_value = Input::post( 'inputValue', '' );
1537
1538 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1539 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1540 }
1541
1542 $answer = $wpdb->get_row(
1543 $wpdb->prepare(
1544 "SELECT *
1545 FROM {$wpdb->prefix}tutor_quiz_question_answers
1546 WHERE answer_id = %d
1547 LIMIT 0,1 ;
1548 ",
1549 $answer_id
1550 )
1551 );
1552 if ( 'single_choice' === $answer->belongs_question_type ) {
1553 $wpdb->update(
1554 $wpdb->prefix . 'tutor_quiz_question_answers',
1555 array( 'is_correct' => 0 ),
1556 array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) )
1557 );
1558 }
1559 $wpdb->update(
1560 $wpdb->prefix . 'tutor_quiz_question_answers',
1561 array( 'is_correct' => esc_sql( $input_value ) ),
1562 array( 'answer_id' => esc_sql( $answer_id ) )
1563 );
1564 }
1565
1566 /**
1567 * Rendering quiz for frontend
1568 *
1569 * @since 1.0.0
1570 *
1571 * @return void send wp_json response
1572 */
1573 public function tutor_render_quiz_content() {
1574
1575 tutor_utils()->checking_nonce();
1576
1577 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1578
1579 if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
1580 wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
1581 }
1582
1583 ob_start();
1584 global $post;
1585
1586 $post = get_post( $quiz_id );
1587 setup_postdata( $post );
1588
1589 single_quiz_contents();
1590 wp_reset_postdata();
1591
1592 $html = ob_get_clean();
1593 wp_send_json_success( array( 'html' => $html ) );
1594 }
1595
1596 /**
1597 * Get attempt details
1598 *
1599 * @since 1.0.0
1600 *
1601 * @param int $attempt_id required attempt id to get details.
1602 *
1603 * @return mixed object on success, null on failure
1604 */
1605 public static function attempt_details( int $attempt_id ) {
1606 global $wpdb;
1607 $attempt_details = $wpdb->get_row(
1608 $wpdb->prepare(
1609 "SELECT *
1610 FROM {$wpdb->prefix}tutor_quiz_attempts
1611 WHERE attempt_id = %d
1612 ",
1613 $attempt_id
1614 )
1615 );
1616 return $attempt_details;
1617 }
1618
1619 /**
1620 * Update quiz attempt info
1621 *
1622 * @since 1.0.0
1623 *
1624 * @param int $attempt_id attempt id.
1625 * @param mixed $attempt_info serialize data.
1626 *
1627 * @return bool, true on success, false on failure
1628 */
1629 public static function update_attempt_info( int $attempt_id, $attempt_info ) {
1630 global $wpdb;
1631 $table = $wpdb->prefix . 'tutor_quiz_attempts';
1632 $update_info = $wpdb->update(
1633 $table,
1634 array( 'attempt_info' => $attempt_info ),
1635 array( 'attempt_id' => $attempt_id )
1636 );
1637 return $update_info ? true : false;
1638 }
1639
1640 /**
1641 * Attempt delete ajax request handler
1642 *
1643 * @since 2.1.0
1644 *
1645 * @return void wp_json response
1646 */
1647 public function attempt_delete() {
1648 tutor_utils()->checking_nonce();
1649 if ( current_user_can( 'administrator' ) || current_user_can( tutor()->instructor_role ) ) {
1650 $attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
1651 if ( $attempt_id ) {
1652 QuizModel::delete_quiz_attempt( $attempt_id );
1653 wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
1654 } else {
1655 wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
1656 }
1657 } else {
1658 wp_send_json_error( __( 'You are not authorized to perform this action!', 'tutor' ) );
1659 }
1660 }
1661
1662 }
1663