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