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