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