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