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