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