PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.1.4
Tutor LMS – eLearning and online course solution v2.1.4
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
1631 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 // Make sure the quiz has answers.
1052 if ( isset( $question['question_type'] ) && in_array( $question['question_type'], $requires_answeres ) ) {
1053 $require_correct = in_array( $question['question_type'], $need_correct );
1054 $all_answers = $this->get_answers_by_q_id( $question_id, $question['question_type'] );
1055 $correct_answers = $this->get_answers_by_q_id( $question_id, $question['question_type'], $require_correct );
1056
1057 if ( ! empty( $all_answers ) && empty( $correct_answers ) ) {
1058 wp_send_json_error( array( 'message' => __( 'Please make sure the question has answer' ) ) );
1059 exit;
1060 }
1061 }
1062
1063 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1064 continue;
1065 }
1066 // Data already sanitize above.
1067 $question_title = $question['question_title'] ?? '';
1068 $question_description = $question['question_description'];
1069 $question_type = $question['question_type'] ?? '';
1070 $question_mark = $question['question_mark'] ?? '';
1071
1072 unset( $question['question_title'] );
1073 unset( $question['question_description'] );
1074
1075 $data = array(
1076 'question_title' => $question_title,
1077 'question_description' => $question_description,
1078 'question_type' => $question_type,
1079 'question_mark' => $question_mark,
1080 'question_settings' => maybe_serialize( $question ),
1081 );
1082
1083 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );
1084 }
1085
1086 wp_send_json_success();
1087 }
1088
1089 /**
1090 * Delete quiz questions
1091 *
1092 * @since 1.0.0
1093 *
1094 * @return void
1095 */
1096 public function tutor_quiz_builder_question_delete() {
1097 tutor_utils()->checking_nonce();
1098
1099 global $wpdb;
1100
1101 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1102
1103 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1104 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1105 }
1106
1107 if ( $question_id ) {
1108 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => esc_sql( $question_id ) ) );
1109 }
1110
1111 wp_send_json_success();
1112 }
1113
1114 /**
1115 * Get answers options form for quiz question
1116 *
1117 * @since 1.0.0
1118 *
1119 * @return void send wp_json response
1120 */
1121 public function tutor_quiz_question_answer_editor() {
1122 tutor_utils()->checking_nonce();
1123
1124 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1125 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1126 $quiz_option = isset( $_POST['tutor_quiz_question'] ) ? tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ) : array(); //phpcs:ignore
1127 $question = tutor_utils()->avalue_dot( $question_id, $quiz_option );
1128 $question_type = $question['question_type'];
1129
1130 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1131 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1132 }
1133
1134 if ( $answer_id ) {
1135 $old_answer = tutor_utils()->get_answer_by_id( $answer_id );
1136 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
1137 foreach ( $old_answer as $old_answer ) {
1138 }
1139 }
1140
1141 ob_start();
1142 include tutor()->path . 'views/modal/question_answer_form.php';
1143 $output = ob_get_clean();
1144
1145 wp_send_json_success( array( 'output' => $output ) );
1146 }
1147
1148 /**
1149 * Undocumented function
1150 *
1151 * @since 1.0.0
1152 *
1153 * @param mixed $questions questions.
1154 * @param mixed $answers answers.
1155 * @param boolean $response should send json response.
1156 *
1157 * @return void
1158 */
1159 public function tutor_save_quiz_answer_options( $questions = null, $answers = null, $response = true ) {
1160 tutor_utils()->checking_nonce();
1161
1162 global $wpdb;
1163 $questions = $questions ? $questions : tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1164 $answers = $answers ? $answers : tutor_utils()->sanitize_array( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1165
1166 foreach ( $answers as $question_id => $answer ) {
1167 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1168 continue;
1169 }
1170
1171 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1172 $question_type = $question['question_type'];
1173
1174 // Getting next sorting order.
1175 $next_order_id = (int) $wpdb->get_var(
1176 $wpdb->prepare(
1177 "SELECT MAX(answer_order)
1178 FROM {$wpdb->prefix}tutor_quiz_question_answers
1179 WHERE belongs_question_id = %d
1180 AND belongs_question_type = %s
1181 ",
1182 $question_id,
1183 esc_sql( $question_type )
1184 )
1185 );
1186
1187 //phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1188 $next_order_id = $next_order_id + 1;
1189
1190 if ( $question ) {
1191 if ( 'true_false' === $question_type ) {
1192 $wpdb->delete(
1193 $wpdb->prefix . 'tutor_quiz_question_answers',
1194 array(
1195 'belongs_question_id' => $question_id,
1196 'belongs_question_type' => $question_type,
1197 )
1198 );
1199 $data_true_false = array(
1200 array(
1201 'belongs_question_id' => esc_sql( $question_id ),
1202 'belongs_question_type' => $question_type,
1203 'answer_title' => __( 'True', 'tutor' ),
1204 'is_correct' => 'true' == $answer['true_false'] ? 1 : 0,
1205 'answer_two_gap_match' => 'true',
1206 ),
1207 array(
1208 'belongs_question_id' => esc_sql( $question_id ),
1209 'belongs_question_type' => $question_type,
1210 'answer_title' => __( 'False', 'tutor' ),
1211 'is_correct' => 'false' === $answer['true_false'] ? 1 : 0,
1212 'answer_two_gap_match' => 'false',
1213 ),
1214 );
1215
1216 foreach ( $data_true_false as $true_false_data ) {
1217 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
1218 }
1219 } elseif ( 'multiple_choice' === $question_type ||
1220 'single_choice' === $question_type ||
1221 'ordering' === $question_type ||
1222 'matching' === $question_type ||
1223 'image_matching' === $question_type ||
1224 'image_answering' === $question_type ) {
1225
1226 $answer_data = array(
1227 'belongs_question_id' => sanitize_text_field( $question_id ),
1228 'belongs_question_type' => $question_type,
1229 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1230 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1231 'answer_view_format' => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
1232 'answer_order' => $next_order_id,
1233 );
1234 if ( isset( $answer['matched_answer_title'] ) ) {
1235 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1236 }
1237
1238 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1239
1240 } elseif ( 'fill_in_the_blank' === $question_type ) {
1241 $wpdb->delete(
1242 $wpdb->prefix . 'tutor_quiz_question_answers',
1243 array(
1244 'belongs_question_id' => $question_id,
1245 'belongs_question_type' => $question_type,
1246 )
1247 );
1248 $answer_data = array(
1249 'belongs_question_id' => sanitize_text_field( $question_id ),
1250 'belongs_question_type' => $question_type,
1251 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1252 'answer_two_gap_match' => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
1253 );
1254 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1255 }
1256 }
1257 }
1258
1259 // Send response to browser if not internal call.
1260 if ( $response ) {
1261 wp_send_json_success();
1262 exit;
1263 }
1264 }
1265
1266 /**
1267 * Tutor Update Answer
1268 *
1269 * @since 1.0.0
1270 *
1271 * @return void send wp_json response
1272 */
1273 public function tutor_update_quiz_answer_options() {
1274 tutor_utils()->checking_nonce();
1275
1276 global $wpdb;
1277
1278 $answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );
1279
1280 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1281 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1282 }
1283
1284 // Data sanitizing by helper method.
1285 $questions = tutor_sanitize_data( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1286 $answers = tutor_sanitize_data( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1287
1288 foreach ( $answers as $question_id => $answer ) {
1289 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1290 $question_type = $question['question_type'];
1291
1292 if ( $question ) {
1293 if ( 'multiple_choice' === $question_type ||
1294 'single_choice' === $question_type ||
1295 'ordering' === $question_type ||
1296 'matching' === $question_type ||
1297 'image_matching' === $question_type ||
1298 'fill_in_the_blank' === $question_type ||
1299 'image_answering' === $question_type ) {
1300
1301 $answer_data = array(
1302 'belongs_question_id' => $question_id,
1303 'belongs_question_type' => $question_type,
1304 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1305 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1306 'answer_view_format' => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
1307 );
1308 if ( isset( $answer['matched_answer_title'] ) ) {
1309 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1310 }
1311
1312 if ( 'fill_in_the_blank' === $question_type ) {
1313 $answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
1314 }
1315
1316 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
1317 }
1318 }
1319 }
1320 wp_send_json_success();
1321 }
1322
1323 /**
1324 * Get answers by quiz id
1325 *
1326 * @since 1.0.0
1327 *
1328 * @param int $question_id question id.
1329 * @param mixed $question_type type of question.
1330 * @param boolean $is_correct only correct answers or not.
1331 *
1332 * @return wpdb:get_results
1333 */
1334 private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
1335 global $wpdb;
1336
1337 $correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1338
1339 return $wpdb->get_results(
1340 $wpdb->prepare(
1341 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1342 WHERE belongs_question_id = %d
1343 AND belongs_question_type = %s
1344 {$correct_clause}
1345 ORDER BY answer_order ASC;
1346 ",
1347 $question_id,
1348 esc_sql( $question_type )
1349 )
1350 );
1351 }
1352
1353 /**
1354 * Quiz builder changed type
1355 *
1356 * @since 1.0.0
1357 *
1358 * @return void send wp_json response
1359 */
1360 public function tutor_quiz_builder_change_type() {
1361 tutor_utils()->checking_nonce();
1362
1363 global $wpdb;
1364 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1365 $question_type = Input::post( 'question_type' );
1366
1367 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1368 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1369 }
1370
1371 // Get question data by question ID.
1372 $question = $wpdb->get_row(
1373 $wpdb->prepare(
1374 "SELECT *
1375 FROM {$wpdb->prefix}tutor_quiz_questions
1376 WHERE question_id = %d
1377 ",
1378 $question_id
1379 )
1380 );
1381
1382 // Get answers by question ID.
1383 $answers = $this->get_answers_by_q_id( $question_id, $question_type );
1384
1385 ob_start();
1386 require tutor()->path . '/views/modal/question_answer_list.php';
1387 $output = ob_get_clean();
1388
1389 wp_send_json_success( array( 'output' => $output ) );
1390 }
1391
1392 /**
1393 * Delete quiz question's answer
1394 *
1395 * @since 1.0.0
1396 *
1397 * @return void send wp_json response
1398 */
1399 public function tutor_quiz_builder_delete_answer() {
1400 tutor_utils()->checking_nonce();
1401
1402 global $wpdb;
1403 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1404
1405 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1406 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1407 }
1408
1409 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => esc_sql( $answer_id ) ) );
1410 wp_send_json_success();
1411 }
1412
1413 /**
1414 * Save quiz questions sorting
1415 *
1416 * @since 1.0.0
1417 *
1418 * @return void
1419 */
1420 public function tutor_quiz_question_sorting() {
1421 tutor_utils()->checking_nonce();
1422
1423 global $wpdb;
1424
1425 // Data sanitizing by helper method.
1426 $question_ids = tutor_utils()->avalue_dot( 'sorted_question_ids', tutor_sanitize_data( $_POST ) ); //phpcs:ignore
1427 if ( is_array( $question_ids ) && count( $question_ids ) ) {
1428 $i = 0;
1429 foreach ( $question_ids as $key => $question_id ) {
1430 if ( tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1431 $i++;
1432 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_order' => $i ), array( 'question_id' => $question_id ) );
1433 }
1434 }
1435 }
1436 }
1437
1438 /**
1439 * Save sorting data for quiz answers
1440 *
1441 * @since 1.0.0
1442 *
1443 * @return void
1444 */
1445 public function tutor_quiz_answer_sorting() {
1446 tutor_utils()->checking_nonce();
1447
1448 global $wpdb;
1449 $answer_ids = Input::post( 'sorted_answer_ids', array(), Input::TYPE_ARRAY );
1450 if ( count( $answer_ids ) ) {
1451 $i = 0;
1452 foreach ( $answer_ids as $key => $answer_id ) {
1453 if ( tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1454 $i++;
1455 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_order' => $i ), array( 'answer_id' => $answer_id ) );
1456 }
1457 }
1458 }
1459 }
1460
1461 /**
1462 * Mark answer as correct
1463 *
1464 * @since 1.0.0
1465 *
1466 * @return void
1467 */
1468 public function tutor_mark_answer_as_correct() {
1469 tutor_utils()->checking_nonce();
1470
1471 global $wpdb;
1472
1473 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1474 // get question info.
1475 $belong_question = $wpdb->get_row(
1476 $wpdb->prepare(
1477 " SELECT belongs_question_id, belongs_question_type
1478 FROM {$wpdb->tutor_quiz_question_answers}
1479 WHERE answer_id = %d
1480 LIMIT 1
1481 ",
1482 $answer_id
1483 )
1484 );
1485 if ( $belong_question ) {
1486 // if question found update all answer is_correct to 0 except post answer.
1487 $question_type = $belong_question->belongs_question_type;
1488 $question_id = $belong_question->belongs_question_id;
1489 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
1490 $update = $wpdb->query(
1491 $wpdb->prepare(
1492 "UPDATE {$wpdb->tutor_quiz_question_answers}
1493 SET is_correct = 0
1494 WHERE belongs_question_id = %d
1495 AND answer_id != %d
1496 ",
1497 $question_id,
1498 $answer_id
1499 )
1500 );
1501 }
1502 }
1503
1504 $input_value = Input::post( 'inputValue', '' );
1505
1506 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1507 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1508 }
1509
1510 $answer = $wpdb->get_row(
1511 $wpdb->prepare(
1512 "SELECT *
1513 FROM {$wpdb->prefix}tutor_quiz_question_answers
1514 WHERE answer_id = %d
1515 LIMIT 0,1 ;
1516 ",
1517 $answer_id
1518 )
1519 );
1520 if ( 'single_choice' === $answer->belongs_question_type ) {
1521 $wpdb->update(
1522 $wpdb->prefix . 'tutor_quiz_question_answers',
1523 array( 'is_correct' => 0 ),
1524 array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) )
1525 );
1526 }
1527 $wpdb->update(
1528 $wpdb->prefix . 'tutor_quiz_question_answers',
1529 array( 'is_correct' => esc_sql( $input_value ) ),
1530 array( 'answer_id' => esc_sql( $answer_id ) )
1531 );
1532 }
1533
1534 /**
1535 * Rendering quiz for frontend
1536 *
1537 * @since 1.0.0
1538 *
1539 * @return void send wp_json response
1540 */
1541 public function tutor_render_quiz_content() {
1542
1543 tutor_utils()->checking_nonce();
1544
1545 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1546
1547 if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
1548 wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
1549 }
1550
1551 ob_start();
1552 global $post;
1553
1554 $post = get_post( $quiz_id );
1555 setup_postdata( $post );
1556
1557 single_quiz_contents();
1558 wp_reset_postdata();
1559
1560 $html = ob_get_clean();
1561 wp_send_json_success( array( 'html' => $html ) );
1562 }
1563
1564 /**
1565 * Get attempt details
1566 *
1567 * @since 1.0.0
1568 *
1569 * @param int $attempt_id required attempt id to get details.
1570 *
1571 * @return mixed object on success, null on failure
1572 */
1573 public static function attempt_details( int $attempt_id ) {
1574 global $wpdb;
1575 $attempt_details = $wpdb->get_row(
1576 $wpdb->prepare(
1577 "SELECT *
1578 FROM {$wpdb->prefix}tutor_quiz_attempts
1579 WHERE attempt_id = %d
1580 ",
1581 $attempt_id
1582 )
1583 );
1584 return $attempt_details;
1585 }
1586
1587 /**
1588 * Update quiz attempt info
1589 *
1590 * @since 1.0.0
1591 *
1592 * @param int $attempt_id attempt id.
1593 * @param mixed $attempt_info serialize data.
1594 *
1595 * @return bool, true on success, false on failure
1596 */
1597 public static function update_attempt_info( int $attempt_id, $attempt_info ) {
1598 global $wpdb;
1599 $table = $wpdb->prefix . 'tutor_quiz_attempts';
1600 $update_info = $wpdb->update(
1601 $table,
1602 array( 'attempt_info' => $attempt_info ),
1603 array( 'attempt_id' => $attempt_id )
1604 );
1605 return $update_info ? true : false;
1606 }
1607
1608 /**
1609 * Attempt delete ajax request handler
1610 *
1611 * @since 2.1.0
1612 *
1613 * @return void wp_json response
1614 */
1615 public function attempt_delete() {
1616 tutor_utils()->checking_nonce();
1617 if ( current_user_can( 'administrator' ) || current_user_can( tutor()->instructor_role ) ) {
1618 $attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
1619 if ( $attempt_id ) {
1620 QuizModel::delete_quiz_attempt( $attempt_id );
1621 wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
1622 } else {
1623 wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
1624 }
1625 } else {
1626 wp_send_json_error( __( 'You are not authorized to perform this action!', 'tutor' ) );
1627 }
1628 }
1629
1630 }
1631