PluginProbe ʕ •ᴥ•ʔ
Tutor LMS – eLearning and online course solution / 2.7.4
Tutor LMS – eLearning and online course solution v2.7.4
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 1 year ago Announcements.php 3 years ago Assets.php 2 years ago Backend_Page_Trait.php 3 years ago Course.php 1 year ago Course_Embed.php 3 years ago Course_Filter.php 1 year ago Course_List.php 1 year 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 1 year ago Lesson.php 1 year ago Options_V2.php 1 year ago Permalink.php 2 years ago Post_types.php 2 years ago Private_Course_Access.php 3 years ago Q_And_A.php 1 year ago Question_Answers_List.php 3 years ago Quiz.php 1 year 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 1 year 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 1 year 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
1833 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 if ( 0 !== $topic_id && 0 !== $ex_quiz_id ) {
967 if ( ! tutor_utils()->can_user_manage( 'quiz', $ex_quiz_id ) ) {
968 wp_send_json_error( array( 'message' => tutor_utils()->error_message() ) );
969 }
970 }
971
972 // Prepare quiz data to save in database.
973 $post_arr = array(
974 'ID' => $ex_quiz_id,
975 'post_type' => 'tutor_quiz',
976 'post_title' => $quiz_title,
977 'post_content' => $quiz_description,
978 'post_status' => 'publish',
979 'post_author' => get_current_user_id(),
980 'post_parent' => $topic_id,
981 'menu_order' => $next_order_id,
982 );
983
984 // Insert quiz and run hook.
985 $quiz_id = wp_insert_post( $post_arr );
986 do_action( ( $ex_quiz_id ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );
987
988 // Sanitize by helper method & save quiz settings.
989 $quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
990 update_post_meta( $quiz_id, 'tutor_quiz_option', $quiz_option );
991 do_action( 'tutor_quiz_settings_updated', $quiz_id );
992
993 // Generate quiz modal to show in modal.
994 $output = $this->tutor_load_quiz_builder_modal(
995 array(
996 'topic_id' => $topic_id,
997 'quiz_id' => $quiz_id,
998 ),
999 true
1000 );
1001
1002 // Generate quiz list to show under topic as sub list.
1003 ob_start();
1004 tutor_load_template_from_custom_path(
1005 tutor()->path . '/views/fragments/quiz-list-single.php',
1006 array(
1007 'quiz_id' => $quiz_id,
1008 'topic_id' => $topic_id,
1009 'quiz_title' => $quiz_title,
1010 ),
1011 false
1012 );
1013 $output_quiz_row = ob_get_clean();
1014
1015 wp_send_json_success(
1016 array(
1017 'output' => $output,
1018 'output_quiz_row' => $output_quiz_row,
1019 )
1020 );
1021 }
1022
1023 /**
1024 * Delete quiz by id
1025 *
1026 * @since 1.0.0
1027 *
1028 * @return void
1029 */
1030 public function tutor_delete_quiz_by_id() {
1031 tutor_utils()->checking_nonce();
1032
1033 global $wpdb;
1034
1035 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1036 $post = get_post( $quiz_id );
1037
1038 if ( ! tutils()->can_user_manage( 'quiz', $quiz_id ) ) {
1039 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1040 }
1041
1042 if ( 'tutor_quiz' === $post->post_type ) {
1043 do_action( 'tutor_delete_quiz_before', $quiz_id );
1044
1045 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
1046 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );
1047
1048 $questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );
1049
1050 if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
1051 $in_question_ids = QueryHelper::prepare_in_clause( $questions_ids );
1052 //phpcs:disable
1053 $wpdb->query(
1054 "DELETE
1055 FROM {$wpdb->prefix}tutor_quiz_question_answers
1056 WHERE belongs_question_id IN({$in_question_ids})
1057 "
1058 );
1059 //phpcs:enable
1060 }
1061
1062 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );
1063
1064 wp_delete_post( $quiz_id, true );
1065
1066 do_action( 'tutor_delete_quiz_after', $quiz_id );
1067
1068 wp_send_json_success();
1069 }
1070
1071 wp_send_json_error();
1072 }
1073
1074 /**
1075 * Load quiz Modal on add/edit click
1076 *
1077 * @since 1.0.0
1078 *
1079 * @param array $params params.
1080 * @param boolean $return should return or not.
1081 *
1082 * @return mixed
1083 */
1084 public function tutor_load_quiz_builder_modal( $params = array(), $return = false ) {
1085 tutor_utils()->checking_nonce();
1086
1087 //phpcs:ignore WordPress.Security.NonceVerification.Missing
1088 $data = array_merge( $_POST, $params );
1089 $quiz_id = isset( $data['quiz_id'] ) ? sanitize_text_field( $data['quiz_id'] ) : 0;
1090 $topic_id = isset( $data['topic_id'] ) ? sanitize_text_field( $data['topic_id'] ) : 0;
1091 $quiz = $quiz_id ? get_post( $quiz_id ) : null;
1092 $course_id = Input::post( 'course_id', 0, Input::TYPE_INT );
1093
1094 if ( $quiz_id && ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1095 wp_send_json_error( array( 'message' => __( 'Quiz Permission Denied', 'tutor' ) ) );
1096 }
1097
1098 ob_start();
1099 include tutor()->path . 'views/modal/edit_quiz.php';
1100 $output = ob_get_clean();
1101
1102 if ( $return ) {
1103 return $output;
1104 }
1105
1106 wp_send_json_success( array( 'output' => $output ) );
1107 }
1108
1109 /**
1110 * Load quiz question form for quiz
1111 *
1112 * @since 1.0.0
1113 *
1114 * @return void
1115 */
1116 public function tutor_quiz_builder_get_question_form() {
1117 tutor_utils()->checking_nonce();
1118
1119 global $wpdb;
1120 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1121 $topic_id = Input::post( 'topic_id', 0, Input::TYPE_INT );
1122 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1123
1124 // Check if the user can manage the quiz.
1125 if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
1126 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1127 }
1128
1129 // If question ID not provided, then create new before rendering the form.
1130 if ( ! $question_id ) {
1131 $next_question_id = QuizModel::quiz_next_question_id();
1132 $next_question_order = QuizModel::quiz_next_question_order_id( $quiz_id );
1133 $question_title = __( 'Question', 'tutor' ) . ' ' . $next_question_id;
1134
1135 $new_question_data = array(
1136 'quiz_id' => $quiz_id,
1137 'question_title' => $question_title,
1138 'question_description' => '',
1139 'question_type' => 'true_false',
1140 'question_mark' => 1,
1141 'question_settings' => maybe_serialize( array() ),
1142 'question_order' => esc_sql( $next_question_order ),
1143 );
1144
1145 $new_question_data = apply_filters( 'tutor_quiz_question_data', $new_question_data );
1146
1147 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_questions', $new_question_data );
1148 $question_id = $wpdb->insert_id;
1149
1150 // Add default true/false options for this question since it is by default true/false type.
1151 $question_array = array(
1152 $question_id => array(
1153 'Question' => $question_title,
1154 'question_type' => 'true_false',
1155 'question_mark' => '1.00',
1156 'question_description' => '',
1157 ),
1158 );
1159
1160 $answer_array = array(
1161 $question_id => array(
1162 'true_false' => true,
1163 ),
1164 );
1165
1166 $this->tutor_save_quiz_answer_options( $question_array, $answer_array, false );
1167 }
1168
1169 // Now get all data by this question id.
1170 $question = $wpdb->get_row(
1171 $wpdb->prepare(
1172 "SELECT * FROM {$wpdb->prefix}tutor_quiz_questions
1173 WHERE question_id = %d ",
1174 $question_id
1175 )
1176 );
1177
1178 // Render the question form finally.
1179 ob_start();
1180 require tutor()->path . 'views/modal/question_form.php';
1181 $output = ob_get_clean();
1182
1183 wp_send_json_success( array( 'output' => $output ) );
1184 }
1185
1186 /**
1187 * Update quiz modal
1188 *
1189 * @since 1.0.0
1190 *
1191 * @return void
1192 */
1193 public function tutor_quiz_modal_update_question() {
1194 tutor_utils()->checking_nonce();
1195
1196 global $wpdb;
1197 // Sanitize $_POST below before using.
1198 $quiz_question_id = Input::post( 'tutor_quiz_question_id', 0, Input::TYPE_INT );
1199 if ( ! $quiz_question_id ) {
1200 wp_send_json_error( __( 'Invalid quiz question ID', 'tutor' ) );
1201 }
1202
1203 /**
1204 * Sanitize $_POST[tutor_quiz_question] data through array_walk
1205 * it will override & sanitize all the question data.
1206 *
1207 * @since 2.1.3
1208 */
1209 // phpcs:ignore
1210 if ( isset( $_POST['tutor_quiz_question'][ $quiz_question_id ] ) ) {
1211 array_walk(
1212 $_POST['tutor_quiz_question'][ $quiz_question_id ], // phpcs:ignore
1213 function( $v, $k ) use ( $quiz_question_id ) {
1214 if ( 'question_description' === $k ) {
1215 add_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
1216 $_POST['tutor_quiz_question'][ $quiz_question_id ][ $k ] = wp_kses_post( wp_unslash( $v ) );
1217 } else {
1218 $_POST['tutor_quiz_question'][ $quiz_question_id ][ $k ] = sanitize_text_field( wp_unslash( $v ) );
1219 }
1220 }
1221 );
1222 } else {
1223 wp_send_json_error( __( 'Invalid quiz question ID', 'tutor' ) );
1224 }
1225
1226 $question_data = wp_unslash( $_POST['tutor_quiz_question'] ); //phpcs:ignore
1227 $requires_answeres = array(
1228 'multiple_choice',
1229 'single_choice',
1230 'true_false',
1231 'fill_in_the_blank',
1232 'matching',
1233 'image_matching',
1234 'image_answering',
1235 'ordering',
1236 );
1237
1238 $need_correct = array(
1239 'multiple_choice',
1240 'single_choice',
1241 'true_false',
1242 );
1243
1244 foreach ( $question_data as $question_id => $question ) {
1245 // Make sure the quiz has answers.
1246 if ( isset( $question['question_type'] ) && in_array( $question['question_type'], $requires_answeres ) ) {
1247 $require_correct = in_array( $question['question_type'], $need_correct );
1248 $all_answers = $this->get_answers_by_q_id( $question_id, $question['question_type'] );
1249 $correct_answers = $this->get_answers_by_q_id( $question_id, $question['question_type'], $require_correct );
1250
1251 if ( ! empty( $all_answers ) && empty( $correct_answers ) ) {
1252 wp_send_json_error( array( 'message' => __( 'Please make sure the question has answer' ) ) );
1253 exit;
1254 }
1255 }
1256
1257 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1258 continue;
1259 }
1260 // Data already sanitize above.
1261 $question_title = $question['question_title'] ?? '';
1262 $question_description = $question['question_description'];
1263 $question_type = $question['question_type'] ?? '';
1264 $question_mark = $question['question_mark'] ?? '';
1265
1266 unset( $question['question_title'] );
1267 unset( $question['question_description'] );
1268
1269 $data = array(
1270 'question_title' => $question_title,
1271 'question_description' => $question_description,
1272 'question_type' => $question_type,
1273 'question_mark' => $question_mark,
1274 'question_settings' => maybe_serialize( $question ),
1275 );
1276
1277 $data = apply_filters( 'tutor_quiz_question_data', $data );
1278
1279 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );
1280 }
1281
1282 wp_send_json_success();
1283 }
1284
1285 /**
1286 * Delete quiz questions
1287 *
1288 * @since 1.0.0
1289 *
1290 * @return void
1291 */
1292 public function tutor_quiz_builder_question_delete() {
1293 tutor_utils()->checking_nonce();
1294
1295 global $wpdb;
1296
1297 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1298
1299 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1300 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1301 }
1302
1303 if ( $question_id ) {
1304 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => esc_sql( $question_id ) ) );
1305 }
1306
1307 wp_send_json_success();
1308 }
1309
1310 /**
1311 * Get answers options form for quiz question
1312 *
1313 * @since 1.0.0
1314 *
1315 * @return void send wp_json response
1316 */
1317 public function tutor_quiz_question_answer_editor() {
1318 tutor_utils()->checking_nonce();
1319
1320 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1321 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1322 $quiz_option = isset( $_POST['tutor_quiz_question'] ) ? tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ) : array(); //phpcs:ignore
1323 $question = tutor_utils()->avalue_dot( $question_id, $quiz_option );
1324 $question_type = $question['question_type'];
1325
1326 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1327 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1328 }
1329
1330 if ( $answer_id ) {
1331 $old_answer = tutor_utils()->get_answer_by_id( $answer_id );
1332 //phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
1333 foreach ( $old_answer as $old_answer ) {
1334 }
1335 }
1336
1337 ob_start();
1338 include tutor()->path . 'views/modal/question_answer_form.php';
1339 $output = ob_get_clean();
1340
1341 wp_send_json_success( array( 'output' => $output ) );
1342 }
1343
1344 /**
1345 * Undocumented function
1346 *
1347 * @since 1.0.0
1348 *
1349 * @param mixed $questions questions.
1350 * @param mixed $answers answers.
1351 * @param boolean $response should send json response.
1352 *
1353 * @return void
1354 */
1355 public function tutor_save_quiz_answer_options( $questions = null, $answers = null, $response = true ) {
1356 tutor_utils()->checking_nonce();
1357
1358 global $wpdb;
1359 $questions = $questions ? $questions : tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1360 $answers = $answers ? $answers : tutor_utils()->sanitize_array( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1361
1362 foreach ( $answers as $question_id => $answer ) {
1363 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1364 continue;
1365 }
1366
1367 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1368 $question_type = $question['question_type'];
1369
1370 // Getting next sorting order.
1371 $next_order_id = (int) $wpdb->get_var(
1372 $wpdb->prepare(
1373 "SELECT MAX(answer_order)
1374 FROM {$wpdb->prefix}tutor_quiz_question_answers
1375 WHERE belongs_question_id = %d
1376 AND belongs_question_type = %s
1377 ",
1378 $question_id,
1379 esc_sql( $question_type )
1380 )
1381 );
1382
1383 //phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
1384 $next_order_id = $next_order_id + 1;
1385
1386 if ( $question ) {
1387 if ( 'true_false' === $question_type ) {
1388 $wpdb->delete(
1389 $wpdb->prefix . 'tutor_quiz_question_answers',
1390 array(
1391 'belongs_question_id' => $question_id,
1392 'belongs_question_type' => $question_type,
1393 )
1394 );
1395 $data_true_false = array(
1396 array(
1397 'belongs_question_id' => esc_sql( $question_id ),
1398 'belongs_question_type' => $question_type,
1399 'answer_title' => __( 'True', 'tutor' ),
1400 'is_correct' => 'true' == $answer['true_false'] ? 1 : 0,
1401 'answer_two_gap_match' => 'true',
1402 ),
1403 array(
1404 'belongs_question_id' => esc_sql( $question_id ),
1405 'belongs_question_type' => $question_type,
1406 'answer_title' => __( 'False', 'tutor' ),
1407 'is_correct' => 'false' === $answer['true_false'] ? 1 : 0,
1408 'answer_two_gap_match' => 'false',
1409 ),
1410 );
1411
1412 foreach ( $data_true_false as $true_false_data ) {
1413 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
1414 }
1415 } elseif ( 'multiple_choice' === $question_type ||
1416 'single_choice' === $question_type ||
1417 'ordering' === $question_type ||
1418 'matching' === $question_type ||
1419 'image_matching' === $question_type ||
1420 'image_answering' === $question_type ) {
1421
1422 $answer_data = array(
1423 'belongs_question_id' => sanitize_text_field( $question_id ),
1424 'belongs_question_type' => $question_type,
1425 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1426 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1427 'answer_view_format' => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
1428 'answer_order' => $next_order_id,
1429 );
1430 if ( isset( $answer['matched_answer_title'] ) ) {
1431 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1432 }
1433
1434 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1435
1436 } elseif ( 'fill_in_the_blank' === $question_type ) {
1437 $wpdb->delete(
1438 $wpdb->prefix . 'tutor_quiz_question_answers',
1439 array(
1440 'belongs_question_id' => $question_id,
1441 'belongs_question_type' => $question_type,
1442 )
1443 );
1444 $answer_data = array(
1445 'belongs_question_id' => sanitize_text_field( $question_id ),
1446 'belongs_question_type' => $question_type,
1447 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1448 'answer_two_gap_match' => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
1449 );
1450 $wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
1451 }
1452 }
1453 }
1454
1455 // Send response to browser if not internal call.
1456 if ( $response ) {
1457 wp_send_json_success();
1458 exit;
1459 }
1460 }
1461
1462 /**
1463 * Tutor Update Answer
1464 *
1465 * @since 1.0.0
1466 *
1467 * @return void send wp_json response
1468 */
1469 public function tutor_update_quiz_answer_options() {
1470 tutor_utils()->checking_nonce();
1471
1472 global $wpdb;
1473
1474 $answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );
1475
1476 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1477 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1478 }
1479
1480 // Data sanitizing by helper method.
1481 $questions = tutor_sanitize_data( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
1482 $answers = tutor_sanitize_data( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore
1483
1484 foreach ( $answers as $question_id => $answer ) {
1485 $question = tutor_utils()->avalue_dot( $question_id, $questions );
1486 $question_type = $question['question_type'];
1487
1488 if ( $question ) {
1489 if ( 'multiple_choice' === $question_type ||
1490 'single_choice' === $question_type ||
1491 'ordering' === $question_type ||
1492 'matching' === $question_type ||
1493 'image_matching' === $question_type ||
1494 'fill_in_the_blank' === $question_type ||
1495 'image_answering' === $question_type ) {
1496
1497 $answer_data = array(
1498 'belongs_question_id' => $question_id,
1499 'belongs_question_type' => $question_type,
1500 'answer_title' => sanitize_text_field( $answer['answer_title'] ),
1501 'image_id' => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
1502 'answer_view_format' => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
1503 );
1504 if ( isset( $answer['matched_answer_title'] ) ) {
1505 $answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
1506 }
1507
1508 if ( 'fill_in_the_blank' === $question_type ) {
1509 $answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
1510 }
1511
1512 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
1513 }
1514 }
1515 }
1516 wp_send_json_success();
1517 }
1518
1519 /**
1520 * Get answers by quiz id
1521 *
1522 * @since 1.0.0
1523 *
1524 * @param int $question_id question id.
1525 * @param mixed $question_type type of question.
1526 * @param boolean $is_correct only correct answers or not.
1527 *
1528 * @return wpdb:get_results
1529 */
1530 private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
1531 global $wpdb;
1532
1533 $correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
1534 //phpcs:disable
1535 return $wpdb->get_results(
1536 $wpdb->prepare(
1537 "SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
1538 WHERE belongs_question_id = %d
1539 AND belongs_question_type = %s
1540 {$correct_clause}
1541 ORDER BY answer_order ASC;
1542 ",
1543 $question_id,
1544 esc_sql( $question_type )
1545 )
1546 );
1547 //phpcs:enable
1548 }
1549
1550 /**
1551 * Quiz builder changed type
1552 *
1553 * @since 1.0.0
1554 *
1555 * @return void send wp_json response
1556 */
1557 public function tutor_quiz_builder_change_type() {
1558 tutor_utils()->checking_nonce();
1559
1560 global $wpdb;
1561 $question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
1562 $question_type = Input::post( 'question_type' );
1563
1564 if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1565 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1566 }
1567
1568 // Get question data by question ID.
1569 $question = $wpdb->get_row(
1570 $wpdb->prepare(
1571 "SELECT *
1572 FROM {$wpdb->prefix}tutor_quiz_questions
1573 WHERE question_id = %d
1574 ",
1575 $question_id
1576 )
1577 );
1578
1579 // Get answers by question ID.
1580 $answers = $this->get_answers_by_q_id( $question_id, $question_type );
1581
1582 ob_start();
1583 require tutor()->path . '/views/modal/question_answer_list.php';
1584 $output = ob_get_clean();
1585
1586 wp_send_json_success( array( 'output' => $output ) );
1587 }
1588
1589 /**
1590 * Delete quiz question's answer
1591 *
1592 * @since 1.0.0
1593 *
1594 * @return void send wp_json response
1595 */
1596 public function tutor_quiz_builder_delete_answer() {
1597 tutor_utils()->checking_nonce();
1598
1599 global $wpdb;
1600 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1601
1602 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1603 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1604 }
1605
1606 $wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => esc_sql( $answer_id ) ) );
1607 wp_send_json_success();
1608 }
1609
1610 /**
1611 * Save quiz questions sorting
1612 *
1613 * @since 1.0.0
1614 *
1615 * @return void
1616 */
1617 public function tutor_quiz_question_sorting() {
1618 tutor_utils()->checking_nonce();
1619
1620 global $wpdb;
1621
1622 // Data sanitizing by helper method.
1623 $question_ids = tutor_utils()->avalue_dot( 'sorted_question_ids', tutor_sanitize_data( $_POST ) ); //phpcs:ignore
1624 if ( is_array( $question_ids ) && count( $question_ids ) ) {
1625 $i = 0;
1626 foreach ( $question_ids as $key => $question_id ) {
1627 if ( tutor_utils()->can_user_manage( 'question', $question_id ) ) {
1628 $i++;
1629 $wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_order' => $i ), array( 'question_id' => $question_id ) );
1630 }
1631 }
1632 }
1633 }
1634
1635 /**
1636 * Save sorting data for quiz answers
1637 *
1638 * @since 1.0.0
1639 *
1640 * @return void
1641 */
1642 public function tutor_quiz_answer_sorting() {
1643 tutor_utils()->checking_nonce();
1644
1645 global $wpdb;
1646 $answer_ids = Input::post( 'sorted_answer_ids', array(), Input::TYPE_ARRAY );
1647 if ( count( $answer_ids ) ) {
1648 $i = 0;
1649 foreach ( $answer_ids as $key => $answer_id ) {
1650 if ( tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1651 $i++;
1652 $wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_order' => $i ), array( 'answer_id' => $answer_id ) );
1653 }
1654 }
1655 }
1656 }
1657
1658 /**
1659 * Mark answer as correct
1660 *
1661 * @since 1.0.0
1662 *
1663 * @return void
1664 */
1665 public function tutor_mark_answer_as_correct() {
1666 tutor_utils()->checking_nonce();
1667
1668 global $wpdb;
1669
1670 $answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );
1671 // get question info.
1672 $belong_question = $wpdb->get_row(
1673 $wpdb->prepare(
1674 " SELECT belongs_question_id, belongs_question_type
1675 FROM {$wpdb->tutor_quiz_question_answers}
1676 WHERE answer_id = %d
1677 LIMIT 1
1678 ",
1679 $answer_id
1680 )
1681 );
1682 if ( $belong_question ) {
1683 // if question found update all answer is_correct to 0 except post answer.
1684 $question_type = $belong_question->belongs_question_type;
1685 $question_id = $belong_question->belongs_question_id;
1686 if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
1687 $update = $wpdb->query(
1688 $wpdb->prepare(
1689 "UPDATE {$wpdb->tutor_quiz_question_answers}
1690 SET is_correct = 0
1691 WHERE belongs_question_id = %d
1692 AND answer_id != %d
1693 ",
1694 $question_id,
1695 $answer_id
1696 )
1697 );
1698 }
1699 }
1700
1701 $input_value = Input::post( 'inputValue', '' );
1702
1703 if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
1704 wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
1705 }
1706
1707 $answer = $wpdb->get_row(
1708 $wpdb->prepare(
1709 "SELECT *
1710 FROM {$wpdb->prefix}tutor_quiz_question_answers
1711 WHERE answer_id = %d
1712 LIMIT 0,1 ;
1713 ",
1714 $answer_id
1715 )
1716 );
1717 if ( 'single_choice' === $answer->belongs_question_type ) {
1718 $wpdb->update(
1719 $wpdb->prefix . 'tutor_quiz_question_answers',
1720 array( 'is_correct' => 0 ),
1721 array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) )
1722 );
1723 }
1724 $wpdb->update(
1725 $wpdb->prefix . 'tutor_quiz_question_answers',
1726 array( 'is_correct' => esc_sql( $input_value ) ),
1727 array( 'answer_id' => esc_sql( $answer_id ) )
1728 );
1729 }
1730
1731 /**
1732 * Rendering quiz for frontend
1733 *
1734 * @since 1.0.0
1735 *
1736 * @return void send wp_json response
1737 */
1738 public function tutor_render_quiz_content() {
1739
1740 tutor_utils()->checking_nonce();
1741
1742 $quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
1743
1744 if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
1745 wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
1746 }
1747
1748 ob_start();
1749 global $post;
1750
1751 $post = get_post( $quiz_id ); //phpcs:ignore
1752 setup_postdata( $post );
1753
1754 single_quiz_contents();
1755 wp_reset_postdata();
1756
1757 $html = ob_get_clean();
1758 wp_send_json_success( array( 'html' => $html ) );
1759 }
1760
1761 /**
1762 * Get attempt details
1763 *
1764 * @since 1.0.0
1765 *
1766 * @param int $attempt_id required attempt id to get details.
1767 *
1768 * @return mixed object on success, null on failure
1769 */
1770 public static function attempt_details( int $attempt_id ) {
1771 global $wpdb;
1772 $attempt_details = $wpdb->get_row(
1773 $wpdb->prepare(
1774 "SELECT *
1775 FROM {$wpdb->prefix}tutor_quiz_attempts
1776 WHERE attempt_id = %d
1777 ",
1778 $attempt_id
1779 )
1780 );
1781 return $attempt_details;
1782 }
1783
1784 /**
1785 * Update quiz attempt info
1786 *
1787 * @since 1.0.0
1788 *
1789 * @param int $attempt_id attempt id.
1790 * @param mixed $attempt_info serialize data.
1791 *
1792 * @return bool, true on success, false on failure
1793 */
1794 public static function update_attempt_info( int $attempt_id, $attempt_info ) {
1795 global $wpdb;
1796 $table = $wpdb->prefix . 'tutor_quiz_attempts';
1797 $update_info = $wpdb->update(
1798 $table,
1799 array( 'attempt_info' => $attempt_info ),
1800 array( 'attempt_id' => $attempt_id )
1801 );
1802 return $update_info ? true : false;
1803 }
1804
1805 /**
1806 * Attempt delete ajax request handler
1807 *
1808 * @since 2.1.0
1809 *
1810 * @return void wp_json response
1811 */
1812 public function attempt_delete() {
1813 tutor_utils()->checking_nonce();
1814
1815 $attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
1816 $attempt = tutor_utils()->get_attempt( $attempt_id );
1817 if ( ! $attempt ) {
1818 wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
1819 }
1820
1821 $user_id = get_current_user_id();
1822 $course_id = $attempt->course_id;
1823
1824 if ( tutor_utils()->can_user_edit_course( $user_id, $course_id ) ) {
1825 QuizModel::delete_quiz_attempt( $attempt_id );
1826 wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
1827 } else {
1828 wp_send_json_error( tutor_utils()->error_message() );
1829 }
1830 }
1831
1832 }
1833