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