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