%PDF- %PDF-
Mini Shell

Mini Shell

Direktori : /home/komfo908/treinamento.komforthouse.com.br/wp-content/plugins/tutor/classes/
Upload File :
Create Path :
Current File : /home/komfo908/treinamento.komforthouse.com.br/wp-content/plugins/tutor/classes/Quiz.php

<?php
/**
 * Quiz class
 *
 * @package Tutor\QuestionAnswer
 * @author Themeum <support@themeum.com>
 * @link https://themeum.com
 * @since 1.0.0
 */

namespace TUTOR;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use Tutor\Helpers\HttpHelper;
use Tutor\Helpers\QueryHelper;
use Tutor\Models\CourseModel;
use Tutor\Models\QuizModel;
use Tutor\Traits\JsonResponse;

/**
 * Manage quiz operations.
 *
 * @since 1.0.0
 */
class Quiz {
	use JsonResponse;

	const META_QUIZ_OPTION = 'tutor_quiz_option';

	/**
	 * Allowed attrs
	 *
	 * @var array
	 */
	private $allowed_attributes = array(
		'src'      => array(),
		'style'    => array(),
		'class'    => array(),
		'id'       => array(),
		'href'     => array(),
		'alt'      => array(),
		'title'    => array(),
		'type'     => array(),
		'controls' => array(),
		'muted'    => array(),
		'loop'     => array(),
		'poster'   => array(),
		'preload'  => array(),
		'autoplay' => array(),
		'width'    => array(),
		'height'   => array(),
	);

	/**
	 * Allowed HTML tags
	 *
	 * @var array
	 */
	private $allowed_html = array( 'img', 'b', 'i', 'br', 'a', 'audio', 'video', 'source' );

	/**
	 * Register hooks
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function __construct() {
		add_action( 'save_post_tutor_quiz', array( $this, 'save_quiz_meta' ) );
		add_action( 'wp_ajax_remove_quiz_from_post', array( $this, 'remove_quiz_from_post' ) );

		add_action( 'wp_ajax_tutor_quiz_timeout', array( $this, 'tutor_quiz_timeout' ) );

		// User take the quiz.
		add_action( 'template_redirect', array( $this, 'start_the_quiz' ) );
		add_action( 'template_redirect', array( $this, 'answering_quiz' ) );
		add_action( 'template_redirect', array( $this, 'finishing_quiz_attempt' ) );

		add_action( 'wp_ajax_review_quiz_answer', array( $this, 'review_quiz_answer' ) );
		// Instructor Feedback Action.
		add_action( 'wp_ajax_tutor_instructor_feedback', array( $this, 'tutor_instructor_feedback' ) );

		/**
		 * New Design Quiz
		 */

		add_action( 'wp_ajax_tutor_quiz_save', array( $this, 'ajax_quiz_save' ) );
		add_action( 'wp_ajax_tutor_quiz_delete', array( $this, 'ajax_quiz_delete' ) );
		add_action( 'wp_ajax_tutor_quiz_details', array( $this, 'ajax_quiz_details' ) );

		add_action( 'wp_ajax_tutor_quiz_question_create', array( $this, 'ajax_quiz_question_create' ) );
		add_action( 'wp_ajax_tutor_quiz_question_update', array( $this, 'ajax_quiz_question_update' ) );
		add_action( 'wp_ajax_tutor_quiz_question_delete', array( $this, 'ajax_quiz_question_delete' ) );
		add_action( 'wp_ajax_tutor_quiz_question_sorting', array( $this, 'ajax_quiz_question_sorting' ) );

		add_action( 'wp_ajax_tutor_quiz_question_answer_save', array( $this, 'ajax_quiz_question_answer_save' ) );
		add_action( 'wp_ajax_tutor_quiz_question_answer_delete', array( $this, 'ajax_quiz_question_answer_delete' ) );
		add_action( 'wp_ajax_tutor_quiz_question_answer_sorting', array( $this, 'ajax_quiz_question_answer_sorting' ) );
		add_action( 'wp_ajax_tutor_mark_answer_as_correct', array( $this, 'ajax_mark_answer_as_correct' ) );

		add_action( 'wp_ajax_tutor_load_quiz_builder_modal', array( $this, 'tutor_load_quiz_builder_modal' ), 10, 0 );
		add_action( 'wp_ajax_tutor_quiz_builder_get_question_form', array( $this, 'tutor_quiz_builder_get_question_form' ) );
		add_action( 'wp_ajax_tutor_quiz_modal_update_question', array( $this, 'tutor_quiz_modal_update_question' ) );
		add_action( 'wp_ajax_tutor_quiz_question_answer_editor', array( $this, 'tutor_quiz_question_answer_editor' ) );
		add_action( 'wp_ajax_tutor_save_quiz_answer_options', array( $this, 'tutor_save_quiz_answer_options' ), 10, 0 );
		add_action( 'wp_ajax_tutor_update_quiz_answer_options', array( $this, 'tutor_update_quiz_answer_options' ) );
		add_action( 'wp_ajax_tutor_quiz_builder_change_type', array( $this, 'tutor_quiz_builder_change_type' ) );

		/**
		 * Frontend Stuff
		 */
		add_action( 'wp_ajax_tutor_render_quiz_content', array( $this, 'tutor_render_quiz_content' ) );

		/**
		 * Quiz abandon action
		 *
		 * @since 1.9.6
		 */
		add_action( 'wp_ajax_tutor_quiz_abandon', array( $this, 'tutor_quiz_abandon' ) );

		$this->prepare_allowed_html();

		/**
		 * Delete quiz attempt
		 *
		 * @since 2.1.0
		 */
		add_action( 'wp_ajax_tutor_attempt_delete', array( $this, 'attempt_delete' ) );

		add_action( 'tutor_quiz/answer/review/after', array( $this, 'do_auto_course_complete' ), 10, 3 );
	}

	/**
	 * Get quiz time units options.
	 *
	 * @since 2.6.0
	 *
	 * @return array
	 */
	public static function quiz_time_units() {
		$time_units = array(
			'seconds' => __( 'Seconds', 'tutor' ),
			'minutes' => __( 'Minutes', 'tutor' ),
			'hours'   => __( 'Hours', 'tutor' ),
			'days'    => __( 'Days', 'tutor' ),
			'weeks'   => __( 'Weeks', 'tutor' ),
		);

		return apply_filters( 'tutor_quiz_time_units', $time_units );
	}

	/**
	 * Get quiz default settings.
	 *
	 * @since 3.0.0
	 *
	 * @return array
	 */
	public static function get_default_quiz_settings() {
		$settings = array(
			'time_limit'                         => array(
				'time_type'  => 'minutes',
				'time_value' => 0,
			),
			'attempts_allowed'                   => 10,
			'feedback_mode'                      => 'retry',
			'hide_question_number_overview'      => 0,
			'hide_quiz_time_display'             => 0,
			'max_questions_for_answer'           => 10,
			'open_ended_answer_characters_limit' => 500,
			'pass_is_required'                   => 0,
			'passing_grade'                      => 80,
			'question_layout_view'               => '',
			'questions_order'                    => 'rand',
			'quiz_auto_start'                    => 0,
			'short_answer_characters_limit'      => 200,
		);

		return apply_filters( 'tutor_quiz_default_settings', $settings );
	}

	/**
	 * Get question default settings.
	 *
	 * @since 3.0.0
	 *
	 * @param string $type type of question.
	 *
	 * @return array
	 */
	public static function get_default_question_settings( $type ) {
		$settings = array(
			'question_type'      => $type,
			'question_mark'      => 1,
			'answer_required'    => 0,
			'randomize_options'  => 0,
			'show_question_mark' => 0,
		);

		return apply_filters( 'tutor_question_default_settings', $settings );
	}

	/**
	 * Get quiz modes
	 *
	 * @since 2.6.0
	 *
	 * @return array
	 */
	public static function quiz_modes() {
		$modes = array(
			array(
				'key'         => 'default',
				'value'       => __( 'Default', 'tutor' ),
				'description' => __( 'Answers shown after quiz is finished', 'tutor' ),
			),
			array(
				'key'         => 'reveal',
				'value'       => __( 'Reveal Mode', 'tutor' ),
				'description' => __( 'Show result after the attempt.', 'tutor' ),
			),
			array(
				'key'         => 'retry',
				'value'       => __( 'Retry Mode', 'tutor' ),
				'description' => __( 'Reattempt quiz any number of times. Define Attempts Allowed below.', 'tutor' ),
			),
		);

		return apply_filters( 'tutor_quiz_modes', $modes );
	}

	/**
	 * Get quiz modes
	 *
	 * @since 2.6.0
	 *
	 * @return array
	 */
	public static function quiz_question_layouts() {
		$layouts = array(
			''                          => __( 'Set question layout view', 'tutor' ),
			'single_question'           => __( 'Single Question', 'tutor' ),
			'question_pagination'       => __( 'Question Pagination', 'tutor' ),
			'question_below_each_other' => __( 'Question below each other', 'tutor' ),
		);

		return apply_filters( 'tutor_quiz_layouts', $layouts );
	}

	/**
	 * Get quiz modes
	 *
	 * @since 2.6.0
	 *
	 * @return array
	 */
	public static function quiz_question_orders() {
		$orders = array(
			'rand'    => __( 'Random', 'tutor' ),
			'sorting' => __( 'Sorting', 'tutor' ),
			'asc'     => __( 'Ascending', 'tutor' ),
			'desc'    => __( 'Descending', 'tutor' ),
		);

		return apply_filters( 'tutor_quiz_layouts', $orders );
	}

	/**
	 * Prepare allowed HTML
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	private function prepare_allowed_html() {

		$allowed = array();

		foreach ( $this->allowed_html as $tag ) {
			$allowed[ $tag ] = $this->allowed_attributes;
		}

		$this->allowed_html = $allowed;
	}

	/**
	 * Instructor feedback ajax request handler
	 *
	 * @since 1.0.0
	 *
	 * @return void | send json response
	 */
	public function tutor_instructor_feedback() {
		tutor_utils()->checking_nonce();

		// Check if user is privileged.
		if ( ! User::has_any_role( array( User::ADMIN, User::INSTRUCTOR ) ) ) {
			wp_send_json_error( tutor_utils()->error_message() );
		}

		$attempt_details = self::attempt_details( Input::post( 'attempt_id', 0, Input::TYPE_INT ) );
		$feedback        = Input::post( 'feedback', '', Input::TYPE_KSES_POST );
		$attempt_info    = isset( $attempt_details->attempt_info ) ? $attempt_details->attempt_info : false;
		if ( $attempt_info ) {
			//phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_unserialize
			$unserialized = unserialize( $attempt_details->attempt_info );
			if ( is_array( $unserialized ) ) {
				$unserialized['instructor_feedback'] = $feedback;

				do_action( 'tutor_quiz/attempt/submitted/feedback', $attempt_details->attempt_id );
				//phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
				$update = self::update_attempt_info( $attempt_details->attempt_id, serialize( $unserialized ) );
				if ( $update ) {
					wp_send_json_success();
				} else {
					wp_send_json_error();
				}
			} else {
				wp_send_json_error( __( 'Invalid quiz info' ) );
			}
		}
		wp_send_json_error();
	}

	/**
	 * Update quiz meta
	 *
	 * @since 1.0.0
	 *
	 * @param int $post_ID post id.
	 * @return void
	 */
	public function save_quiz_meta( $post_ID ) {
		//phpcs:ignore WordPress.Security.NonceVerification.Missing
		if ( isset( $_POST['quiz_option'] ) ) {
			$quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
			update_post_meta( $post_ID, 'tutor_quiz_option', $quiz_option );
		}
	}

	/**
	 * Remove quiz from post
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function remove_quiz_from_post() {
		tutor_utils()->checking_nonce();

		global $wpdb;
		$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );

		if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
		}

		$wpdb->update( $wpdb->posts, array( 'post_parent' => 0 ), array( 'ID' => $quiz_id ) );
		wp_send_json_success();
	}

	/**
	 * Start Quiz from here...
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function start_the_quiz() {
		if ( Input::post( 'tutor_action' ) !== 'tutor_start_quiz' ) {
			return;
		}
		// Checking nonce.
		tutor_utils()->checking_nonce();

		if ( ! is_user_logged_in() ) {
			// TODO: need to set a view in the next version.
			die( 'Please sign in to do this operation' );
		}

		$user_id = get_current_user_id();
		$user    = get_userdata( $user_id );

		$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );

		$quiz   = get_post( $quiz_id );
		$course = CourseModel::get_course_by_quiz( $quiz_id );

		self::quiz_attempt( $course->ID, $quiz_id, $user_id );
		wp_safe_redirect( get_permalink( $quiz_id ) );
		die();
	}

	/**
	 * Manage quiz attempt
	 *
	 * @since 2.6.1
	 *
	 * @param integer $course_id course id.
	 * @param integer $quiz_id quiz id.
	 * @param integer $user_id user id.
	 * @param string  $attempt_status attempt status.
	 *
	 * @return int inserted id|0
	 */
	public static function quiz_attempt( int $course_id, int $quiz_id, int $user_id, $attempt_status = 'attempt_started' ) {
		global $wpdb;

		if ( ! $course_id ) {
			die( 'There is something went wrong with course, please check if quiz attached with a course' );
		}

		do_action( 'tutor_quiz/start/before', $quiz_id, $user_id );

		$date = date( 'Y-m-d H:i:s', tutor_time() ); //phpcs:ignore

		$tutor_quiz_option = (array) maybe_unserialize( get_post_meta( $quiz_id, 'tutor_quiz_option', true ) );
		$attempts_allowed  = tutor_utils()->get_quiz_option( $quiz_id, 'attempts_allowed', 0 );

		$time_limit         = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_value' );
		$time_limit_seconds = 0;
		$time_type          = 'seconds';
		if ( $time_limit ) {
			$time_type = tutor_utils()->get_quiz_option( $quiz_id, 'time_limit.time_type' );

			switch ( $time_type ) {
				case 'seconds':
					$time_limit_seconds = $time_limit;
					break;
				case 'minutes':
					$time_limit_seconds = $time_limit * 60;
					break;
				case 'hours':
					$time_limit_seconds = $time_limit * 60 * 60;
					break;
				case 'days':
					$time_limit_seconds = $time_limit * 60 * 60 * 24;
					break;
				case 'weeks':
					$time_limit_seconds = $time_limit * 60 * 60 * 24 * 7;
					break;
			}
		}

		$max_question_allowed                                  = tutor_utils()->max_questions_for_take_quiz( $quiz_id );
		$tutor_quiz_option['time_limit']['time_limit_seconds'] = $time_limit_seconds;

		$attempt_data = array(
			'course_id'                => $course_id,
			'quiz_id'                  => $quiz_id,
			'user_id'                  => $user_id,
			'total_questions'          => $max_question_allowed,
			'total_answered_questions' => 0,
			'attempt_info'             => maybe_serialize( $tutor_quiz_option ),
			'attempt_status'           => $attempt_status,
			'attempt_ip'               => tutor_utils()->get_ip(),
			'attempt_started_at'       => $date,
		);

		$wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_data );
		$attempt_id = (int) $wpdb->insert_id;

		if ( $attempt_id ) {
			do_action( 'tutor_quiz/start/after', $quiz_id, $user_id, $attempt_id );
			return $attempt_id;
		} else {
			return 0;
		}
	}

	/**
	 * Answering quiz
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function answering_quiz() {

		if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
			return;
		}
		// submit quiz attempts.
		self::tutor_quiz_attemp_submit();

		wp_safe_redirect( get_the_permalink() );
		die();
	}

	/**
	 * Quiz abandon submission handler
	 *
	 * @since 1.9.6
	 *
	 * @return JSON response
	 */
	public function tutor_quiz_abandon() {
		if ( Input::post( 'tutor_action' ) !== 'tutor_answering_quiz_question' ) {
			return;
		}
		tutor_utils()->checking_nonce();
		// submit quiz attempts.
		if ( self::tutor_quiz_attemp_submit() ) {
			wp_send_json_success();
		} else {
			wp_send_json_error();
		}
	}

	/**
	 * This is  a unified method for handling normal quiz submit or abandon submit
	 * It will handle ajax or normal form submit and can be used with different hooks
	 *
	 * @since 1.9.6
	 *
	 * @return true | false
	 */
	public static function tutor_quiz_attemp_submit() {
		// Check logged in.
		if ( ! is_user_logged_in() ) {
			die( 'Please sign in to do this operation' );
		}

		// Check nonce.
		tutor_utils()->checking_nonce();

		// Prepare attempt info.
		$user_id    = get_current_user_id();
		$attempt_id = Input::post( 'attempt_id', 0, Input::TYPE_INT );
		$attempt    = tutor_utils()->get_attempt( $attempt_id );
		$course_id  = CourseModel::get_course_by_quiz( $attempt->quiz_id )->ID;

		// Sanitize data by helper method.
		$attempt_answers = isset( $_POST['attempt'] ) ? tutor_sanitize_data( $_POST['attempt'] ) : false; //phpcs:ignore
		$attempt_answers = is_array( $attempt_answers ) ? $attempt_answers : array();

		// Check if has access to the attempt.
		if ( ! $attempt || $user_id != $attempt->user_id ) {
			die( 'Operation not allowed, attempt not found or permission denied' );
		}
		self::manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $course_id, $user_id );
		return true;
	}

	/**
	 * Manage attempt answers
	 *
	 * Evaluate each attempt answer and update the attempts table & insert in the attempt_answers table.
	 *
	 * @since 2.6.1
	 *
	 * @param array  $attempt_answers attempt answers.
	 * @param object $attempt single attempt.
	 * @param int    $attempt_id attempt id.
	 * @param int    $course_id course id.
	 * @param int    $user_id user id.
	 *
	 * @return void
	 */
	public static function manage_attempt_answers( $attempt_answers, $attempt, $attempt_id, $course_id, $user_id ) {
		global $wpdb;
		// Before hook.
		do_action( 'tutor_quiz/attempt_analysing/before', $attempt_id );

		// Single quiz can have multiple question. So multiple answer should be saved.
		foreach ( $attempt_answers as $attempt_id => $attempt_answer ) {
			// Get total marks of all question comes.
			$question_ids = tutor_utils()->avalue_dot( 'quiz_question_ids', $attempt_answer );
			$question_ids = array_filter(
				$question_ids,
				function( $id ) {
					return (int) $id;
				}
			);

			// Calculate and set the total marks in attempt table for this question.
			if ( is_array( $question_ids ) && count( $question_ids ) ) {
				$question_ids_string = QueryHelper::prepare_in_clause( $question_ids );

				// Get total marks of the questions from question table.
				//phpcs:disable
				$query = $wpdb->prepare(
					"SELECT SUM(question_mark)
						FROM {$wpdb->prefix}tutor_quiz_questions
						WHERE 1 = %d
							AND question_id IN({$question_ids_string});
					",
					1
				);
				$total_question_marks = $wpdb->get_var( $query );
				//phpcs:enable

				$total_question_marks = apply_filters( 'tutor_filter_update_before_question_mark', $total_question_marks, $question_ids, $user_id, $attempt_id );

				// Set the the total mark in the attempt table for the question.
				$wpdb->update(
					$wpdb->prefix . 'tutor_quiz_attempts',
					array( 'total_marks' => $total_question_marks ),
					array( 'attempt_id' => $attempt_id )
				);
			}

			$total_marks     = 0;
			$review_required = false;
			$quiz_answers    = tutor_utils()->avalue_dot( 'quiz_question', $attempt_answer );

			if ( tutor_utils()->count( $quiz_answers ) ) {

				foreach ( $quiz_answers as $question_id => $answers ) {
					$question      = QuizModel::get_quiz_question_by_id( $question_id );
					$question_type = $question->question_type;

					$is_answer_was_correct = false;
					$given_answer          = '';

					if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {

						if ( ! is_numeric( $answers ) || ! $answers ) {
							wp_send_json_error();
							exit;
						}

						$given_answer          = $answers;
						$is_answer_was_correct = (bool) $wpdb->get_var(
							$wpdb->prepare(
								"SELECT is_correct
									FROM {$wpdb->prefix}tutor_quiz_question_answers
									WHERE answer_id = %d
								",
								$answers
							)
						);

					} elseif ( 'multiple_choice' === $question_type ) {

						$given_answer = (array) ( $answers );

						$given_answer         = array_filter(
							$given_answer,
							function( $id ) {
								return is_numeric( $id ) && $id > 0;
							}
						);
						$get_original_answers = (array) $wpdb->get_col(
							$wpdb->prepare(
								"SELECT
								answer_id
							FROM
								{$wpdb->prefix}tutor_quiz_question_answers
							WHERE belongs_question_id = %d
								AND belongs_question_type = %s
								AND is_correct = 1 ;
							",
								$question->question_id,
								$question_type
							)
						);

						if ( count( array_diff( $get_original_answers, $given_answer ) ) === 0 && count( $get_original_answers ) === count( $given_answer ) ) {
							$is_answer_was_correct = true;
						}
						$given_answer = maybe_serialize( $answers );

					} elseif ( 'fill_in_the_blank' === $question_type ) {

						$get_original_answer = $wpdb->get_row(
							$wpdb->prepare(
								"SELECT * 
									FROM {$wpdb->prefix}tutor_quiz_question_answers
									WHERE belongs_question_id = %d
										AND belongs_question_type = %s ;
								",
								$question->question_id,
								$question_type
							)
						);

						/**
						 * Answers stored in DB
						 */
						$gap_answer = (array) explode( '|', $get_original_answer->answer_two_gap_match );
						$gap_answer = maybe_serialize(
							array_map(
								function ( $ans ) {
									return wp_slash( trim( $ans ) );
								},
								$gap_answer
							)
						);

						/**
						 * Answers from user input
						 */
						$given_answer = (array) array_map( 'sanitize_text_field', $answers );
						$given_answer = maybe_serialize( $given_answer );

						/**
						 * Compare answer's by making both case-insensitive.
						 */
						if ( strtolower( $given_answer ) == strtolower( $gap_answer ) ) {
							$is_answer_was_correct = true;
						}
					} elseif ( 'open_ended' === $question_type || 'short_answer' === $question_type ) {
						$review_required = true;
						$given_answer    = wp_kses_post( $answers );

					} elseif ( 'ordering' === $question_type || 'matching' === $question_type || 'image_matching' === $question_type ) {

						$given_answer = (array) array_map( 'sanitize_text_field', tutor_utils()->avalue_dot( 'answers', $answers ) );
						$given_answer = maybe_serialize( $given_answer );

						$get_original_answers = (array) $wpdb->get_col(
							$wpdb->prepare(
								"SELECT answer_id
									FROM {$wpdb->prefix}tutor_quiz_question_answers
									WHERE belongs_question_id = %d 
										AND belongs_question_type = %s 
									ORDER BY answer_order ASC ;
								",
								$question->question_id,
								$question_type
							)
						);

						$get_original_answers = array_map( 'sanitize_text_field', $get_original_answers );

						if ( maybe_serialize( $get_original_answers ) == $given_answer ) {
							$is_answer_was_correct = true;
						}
					} elseif ( 'image_answering' === $question_type ) {
						$image_inputs          = tutor_utils()->avalue_dot( 'answer_id', $answers );
						$image_inputs          = (array) array_map( 'sanitize_text_field', $image_inputs );
						$given_answer          = maybe_serialize( $image_inputs );
						$is_answer_was_correct = false;
						/**
						 * For the image_answering question type result
						 * remain pending in spite of correct answer & required
						 * review of admin/instructor. Since it's
						 * pending we need to mark it as incorrect. Otherwise if
						 * mark it correct then earned mark will be updated. then
						 * again when instructor/admin review & mark it as correct
						 * extra mark is adding. In this case, student
						 * getting double mark for the same question.
						 *
						 * For now code is commenting will be removed later on
						 *
						 * @since 2.1.5
						 */

						//phpcs:disable

						// $db_answer = $wpdb->get_col(
						// 	$wpdb->prepare(
						// 		"SELECT answer_title
						// 			FROM {$wpdb->prefix}tutor_quiz_question_answers
						// 			WHERE belongs_question_id = %d
						// 				AND belongs_question_type = 'image_answering'
						// 			ORDER BY answer_order asc ;",
						// 		$question_id
						// 	)
						// );

						// if ( is_array( $db_answer ) && count( $db_answer ) ) {
						// 	$is_answer_was_correct = ( strtolower( maybe_serialize( array_values( $image_inputs ) ) ) == strtolower( maybe_serialize( $db_answer ) ) );
						// }
						//phpcs:enable
					}

					$question_mark = $is_answer_was_correct ? $question->question_mark : 0;
					$total_marks  += $question_mark;

					$total_marks = apply_filters( 'tutor_filter_quiz_total_marks', $total_marks, $question_id, $question_type, $user_id, $attempt_id );

					$answers_data = array(
						'user_id'         => $user_id,
						'quiz_id'         => $attempt->quiz_id,
						'question_id'     => $question_id,
						'quiz_attempt_id' => $attempt_id,
						'given_answer'    => $given_answer,
						'question_mark'   => $question->question_mark,
						'achieved_mark'   => $question_mark,
						'minus_mark'      => 0,
						'is_correct'      => $is_answer_was_correct ? 1 : 0,
					);

					/**
					 * Check if question_type open ended or short ans the set
					 * is_correct default value null before saving
					 */
					if ( in_array( $question_type, array( 'open_ended', 'short_answer', 'image_answering' ) ) ) {
						$answers_data['is_correct'] = null;
						$review_required            = true;
					}

					$answers_data = apply_filters( 'tutor_filter_quiz_answer_data', $answers_data, $question_id, $question_type, $user_id, $attempt_id );

					$wpdb->insert( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answers_data );
				}
			}

			$attempt_info = array(
				'total_answered_questions' => tutor_utils()->count( $quiz_answers ),
				'earned_marks'             => $total_marks,
				'attempt_status'           => 'attempt_ended',
				'attempt_ended_at'         => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
			);

			if ( $review_required ) {
				$attempt_info['attempt_status'] = 'review_required';
			}

			$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
		}

		// After hook.
		do_action( 'tutor_quiz/attempt_ended', $attempt_id, $course_id, $user_id );
	}


	/**
	 * Quiz attempt will be finish here
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function finishing_quiz_attempt() {

		if ( Input::post( 'tutor_action' ) !== 'tutor_finish_quiz_attempt' ) {
			return;
		}
		// Checking nonce.
		tutor_utils()->checking_nonce();

		if ( ! is_user_logged_in() ) {
			die( 'Please sign in to do this operation' );
		}

		global $wpdb;

		$quiz_id    = Input::post( 'quiz_id', 0, Input::TYPE_INT );
		$attempt    = tutor_utils()->is_started_quiz( $quiz_id );
		$attempt_id = $attempt->attempt_id;

		$attempt_info = array(
			'total_answered_questions' => 0,
			'earned_marks'             => 0,
			'attempt_status'           => 'attempt_ended',
			'attempt_ended_at'         => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
		);

		do_action( 'tutor_quiz_before_finish', $attempt_id, $quiz_id, $attempt->user_id );
		$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_info, array( 'attempt_id' => $attempt_id ) );
		do_action( 'tutor_quiz_finished', $attempt_id, $quiz_id, $attempt->user_id );

		wp_redirect( tutor_utils()->input_old( '_wp_http_referer' ) );
	}

	/**
	 * Get quiz total marks.
	 *
	 * @since 3.0.0
	 *
	 * @param int $quiz_id quiz id.
	 *
	 * @return int|float
	 */
	public static function get_quiz_total_marks( $quiz_id ) {
		global $wpdb;

		$total_marks = $wpdb->get_var(
			$wpdb->prepare(
				"SELECT SUM(question_mark) total_marks 
				FROM {$wpdb->prefix}tutor_quiz_questions
				WHERE quiz_id=%d",
				$quiz_id
			)
		);

		return floatval( $total_marks );
	}

	/**
	 * Quiz timeout by ajax
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function tutor_quiz_timeout() {
		tutils()->checking_nonce();

		global $wpdb;

		$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
		$attempt = tutor_utils()->is_started_quiz( $quiz_id );

		if ( $attempt ) {
			$attempt_id = $attempt->attempt_id;

			$data = array(
				'attempt_status'   => 'attempt_timeout',
				'total_marks'      => self::get_quiz_total_marks( $quiz_id ),
				'earned_marks'     => 0,
				'attempt_ended_at' => gmdate( 'Y-m-d H:i:s', tutor_time() ),
			);

			$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $data, array( 'attempt_id' => $attempt->attempt_id ) );

			do_action( 'tutor_quiz_timeout', $attempt_id, $quiz_id, $attempt->user_id );

			wp_send_json_success();
		}

		wp_send_json_error( __( 'Quiz has been timeout already', 'tutor' ) );
	}

	/**
	 * Review quiz answer
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public function review_quiz_answer() {

		tutor_utils()->checking_nonce();

		global $wpdb;

		$attempt_id        = Input::post( 'attempt_id', 0, Input::TYPE_INT );
		$context           = Input::post( 'context' );
		$attempt_answer_id = Input::post( 'attempt_answer_id', 0, Input::TYPE_INT );
		$mark_as           = Input::post( 'mark_as' );

		if ( ! tutor_utils()->can_user_manage( 'attempt', $attempt_id ) || ! tutor_utils()->can_user_manage( 'attempt_answer', $attempt_answer_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
		}

		$attempt_answer = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * 
					FROM {$wpdb->prefix}tutor_quiz_attempt_answers
					WHERE attempt_answer_id = %d
				",
				$attempt_answer_id
			)
		);

		$attempt      = tutor_utils()->get_attempt( $attempt_id );
		$question     = QuizModel::get_quiz_question_by_id( $attempt_answer->question_id );
		$course_id    = $attempt->course_id;
		$student_id   = $attempt->user_id;
		$previous_ans = $attempt_answer->is_correct;

		do_action( 'tutor_quiz_review_answer_before', $attempt_answer_id, $attempt_id, $mark_as );

		if ( 'correct' === $mark_as ) {

			$answer_update_data = array(
				'achieved_mark' => $attempt_answer->question_mark,
				'is_correct'    => 1,
			);
			$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );
			if ( 0 == $previous_ans || null == $previous_ans ) {
				// if previous answer was wrong or in review then add point as correct.
				$attempt_update_data = array(
					'earned_marks'         => $attempt->earned_marks + $attempt_answer->question_mark,
					'is_manually_reviewed' => 1,
					'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ), //phpcs:ignore
				);
			}

			if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
				$attempt_update_data['attempt_status'] = 'attempt_ended';
			}
			$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );

		} elseif ( 'incorrect' === $mark_as ) {

			$answer_update_data = array(
				'achieved_mark' => '0.00',
				'is_correct'    => 0,
			);
			$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempt_answers', $answer_update_data, array( 'attempt_answer_id' => $attempt_answer_id ) );

			if ( 1 == $previous_ans ) {
				// If previous ans was right then mynus.
				$attempt_update_data = array(
					'earned_marks'         => $attempt->earned_marks - $attempt_answer->question_mark,
					'is_manually_reviewed' => 1,
					'manually_reviewed_at' => date( 'Y-m-d H:i:s', tutor_time() ),//phpcs:ignore
				);
			}
			if ( 'open_ended' === $question->question_type || 'short_answer' === $question->question_type ) {
				$attempt_update_data['attempt_status'] = 'attempt_ended';
			}

			$wpdb->update( $wpdb->prefix . 'tutor_quiz_attempts', $attempt_update_data, array( 'attempt_id' => $attempt_id ) );
		}
		do_action( 'tutor_quiz_review_answer_after', $attempt_answer_id, $attempt_id, $mark_as );
		do_action( 'tutor_quiz/answer/review/after', $attempt_answer_id, $course_id, $student_id );

		ob_start();
		tutor_load_template_from_custom_path(
			tutor()->path . '/views/quiz/attempt-details.php',
			array(
				'attempt_id' => $attempt_id,
				'user_id'    => $student_id,
				'context'    => $context,
				'back_url'   => Input::post( 'back_url' ),
			)
		);
		wp_send_json_success( array( 'html' => ob_get_clean() ) );
	}

	/**
	 * Do auto course complete after review a quiz attempt.
	 *
	 * @since 2.4.0
	 *
	 * @param int $attempt_answer_id attempt answer id.
	 * @param int $course_id course id.
	 * @param int $user_id student id.
	 *
	 * @return void
	 */
	public function do_auto_course_complete( $attempt_answer_id, $course_id, $user_id ) {
		if ( CourseModel::can_autocomplete_course( $course_id, $user_id ) ) {
			CourseModel::mark_course_as_completed( $course_id, $user_id );
			Course::set_review_popup_data( $user_id, $course_id );
		}
	}

	/**
	 * Quiz create and update.
	 *
	 * @since 1.0.0
	 * @since 3.0.0 refactor and response change.
	 *
	 * @return void
	 */
	public function ajax_quiz_save() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		$is_update        = false;
		$topic_id         = Input::post( 'topic_id', 0, Input::TYPE_INT );
		$quiz_id          = Input::post( 'quiz_id', 0, Input::TYPE_INT );
		$quiz_title       = Input::post( 'quiz_title' );
		$quiz_description = isset( $_POST['quiz_description'] ) ? wp_kses( wp_unslash( $_POST['quiz_description'] ), $this->allowed_html ) : ''; //phpcs:ignore

		$next_order_id = tutor_utils()->get_next_course_content_order_id( $topic_id, $quiz_id );

		// Check edit privilege.
		if ( ! tutor_utils()->can_user_manage( 'topic', $topic_id ) ) {
			$this->json_response(
				tutor_utils()->error_message(),
				null,
				HttpHelper::STATUS_FORBIDDEN
			);
		}

		if ( 0 !== $topic_id && 0 !== $quiz_id ) {
			if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
				$this->json_response(
					tutor_utils()->error_message(),
					null,
					HttpHelper::STATUS_FORBIDDEN
				);
			}
		}

		// Prepare quiz data to save in database.
		$post_arr = array(
			'post_type'    => 'tutor_quiz',
			'post_title'   => $quiz_title,
			'post_content' => $quiz_description,
			'post_status'  => 'publish',
			'post_author'  => get_current_user_id(),
			'post_parent'  => $topic_id,
			'menu_order'   => $next_order_id,
		);

		if ( $quiz_id ) {
			$is_update      = true;
			$post_arr['ID'] = $quiz_id;
		}

		// Insert quiz and run hook.
		$quiz_id = wp_insert_post( $post_arr );
		do_action( ( $is_update ? 'tutor_quiz_updated' : 'tutor_initial_quiz_created' ), $quiz_id );

		// Sanitize by helper method & save quiz settings.
		$quiz_option = tutor_utils()->sanitize_array( $_POST['quiz_option'] ); //phpcs:ignore
		update_post_meta( $quiz_id, 'tutor_quiz_option', $quiz_option );
		do_action( 'tutor_quiz_settings_updated', $quiz_id );

		if ( $is_update ) {
			$this->json_response(
				__( 'Quiz updated successfully', 'tutor' ),
				$quiz_id
			);
		} else {
			$this->json_response(
				__( 'Quiz created successfully', 'tutor' ),
				$quiz_id,
				HttpHelper::STATUS_CREATED
			);
		}
	}

	/**
	 * Get a quiz details by id
	 *
	 * @return void
	 */
	public function ajax_quiz_details() {
		tutor_utils()->check_nonce();

		$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
		if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
			$this->json_response(
				tutor_utils()->error_message(),
				null,
				HttpHelper::STATUS_FORBIDDEN
			);
		}

		$data = QuizModel::get_quiz_details( $quiz_id );

		$this->json_response(
			__( 'Quiz data fetched successfully', 'tutor' ),
			$data
		);
	}

	/**
	 * Delete quiz by id
	 *
	 * @since 1.0.0
	 * @since 3.0.0 refactor and response change.
	 *
	 * @return void
	 */
	public function ajax_quiz_delete() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		global $wpdb;

		$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );
		if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
			$this->json_response(
				tutor_utils()->error_message(),
				null,
				HttpHelper::STATUS_FORBIDDEN
			);
		}

		$post = get_post( $quiz_id );
		if ( 'tutor_quiz' !== $post->post_type ) {
			$this->json_response(
				__( 'Invalid quiz', 'tutor' ),
				null,
				HttpHelper::STATUS_BAD_REQUEST
			);
		}

		do_action( 'tutor_delete_quiz_before', $quiz_id );

		$wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempts', array( 'quiz_id' => $quiz_id ) );
		$wpdb->delete( $wpdb->prefix . 'tutor_quiz_attempt_answers', array( 'quiz_id' => $quiz_id ) );

		$questions_ids = $wpdb->get_col( $wpdb->prepare( "SELECT question_id FROM {$wpdb->prefix}tutor_quiz_questions WHERE quiz_id = %d ", $quiz_id ) );

		if ( is_array( $questions_ids ) && count( $questions_ids ) ) {
			$in_question_ids = QueryHelper::prepare_in_clause( $questions_ids );
			//phpcs:disable
			$wpdb->query(
				"DELETE 
					FROM {$wpdb->prefix}tutor_quiz_question_answers
					WHERE belongs_question_id IN({$in_question_ids})
				"
			);
			//phpcs:enable
		}

		$wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ) );

		wp_delete_post( $quiz_id, true );

		do_action( 'tutor_delete_quiz_after', $quiz_id );

		$this->json_response(
			__( 'Quiz deleted successfully', 'tutor' ),
			$quiz_id
		);
	}

	/**
	 * Load quiz Modal on add/edit click
	 *
	 * @since 1.0.0
	 *
	 * @param array   $params params.
	 * @param boolean $return should return or not.
	 *
	 * @return mixed
	 */
	public function tutor_load_quiz_builder_modal( $params = array(), $return = false ) {
		tutor_utils()->checking_nonce();

		//phpcs:ignore WordPress.Security.NonceVerification.Missing
		$data      = array_merge( $_POST, $params );
		$quiz_id   = isset( $data['quiz_id'] ) ? sanitize_text_field( $data['quiz_id'] ) : 0;
		$topic_id  = isset( $data['topic_id'] ) ? sanitize_text_field( $data['topic_id'] ) : 0;
		$quiz      = $quiz_id ? get_post( $quiz_id ) : null;
		$course_id = Input::post( 'course_id', 0, Input::TYPE_INT );

		if ( $quiz_id && ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Quiz Permission Denied', 'tutor' ) ) );
		}

		ob_start();
		include tutor()->path . 'views/modal/edit_quiz.php';
		$output = ob_get_clean();

		if ( $return ) {
			return $output;
		}

		wp_send_json_success( array( 'output' => $output ) );
	}

	/**
	 * Delete quiz question
	 *
	 * @since 1.0.0
	 * @since 3.0.0 refactor and response updated.
	 *
	 * @return void
	 */
	public function ajax_quiz_question_delete() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		global $wpdb;

		$question_id = Input::post( 'question_id', 0, Input::TYPE_INT );

		if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
			$this->json_response(
				tutor_utils()->error_message(),
				null,
				HttpHelper::STATUS_FORBIDDEN
			);
		}

		if ( $question_id ) {
			$wpdb->delete( $wpdb->prefix . 'tutor_quiz_questions', array( 'question_id' => $question_id ) );
		}

		$this->json_response(
			__( 'Question successfully deleted', 'tutor' ),
			$question_id
		);

	}

	/**
	 * Get answers options form for quiz question
	 *
	 * @since 1.0.0
	 *
	 * @return void send wp_json response
	 */
	public function tutor_quiz_question_answer_editor() {
		tutor_utils()->checking_nonce();

		$question_id   = Input::post( 'question_id', 0, Input::TYPE_INT );
		$answer_id     = Input::post( 'answer_id', 0, Input::TYPE_INT );
		$quiz_option   = isset( $_POST['tutor_quiz_question'] ) ? tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ) : array(); //phpcs:ignore
		$question      = tutor_utils()->avalue_dot( $question_id, $quiz_option );
		$question_type = $question['question_type'];

		if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
		}

		if ( $answer_id ) {
			$old_answer = tutor_utils()->get_answer_by_id( $answer_id );
			//phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedForeach
			foreach ( $old_answer as $old_answer ) {
			}
		}

		ob_start();
		include tutor()->path . 'views/modal/question_answer_form.php';
		$output = ob_get_clean();

		wp_send_json_success( array( 'output' => $output ) );
	}

	/**
	 * Undocumented function
	 *
	 * @since 1.0.0
	 *
	 * @param mixed   $questions questions.
	 * @param mixed   $answers answers.
	 * @param boolean $response should send json response.
	 *
	 * @return void
	 */
	public function tutor_save_quiz_answer_options( $questions = null, $answers = null, $response = true ) {
		tutor_utils()->checking_nonce();

		global $wpdb;
		$questions = $questions ? $questions : tutor_utils()->sanitize_array( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
		$answers   = $answers ? $answers : tutor_utils()->sanitize_array( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore

		foreach ( $answers as $question_id => $answer ) {
			if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
				continue;
			}

			$question      = tutor_utils()->avalue_dot( $question_id, $questions );
			$question_type = $question['question_type'];

			// Getting next sorting order.
			$next_order_id = (int) $wpdb->get_var(
				$wpdb->prepare(
					"SELECT MAX(answer_order)
						FROM {$wpdb->prefix}tutor_quiz_question_answers
						WHERE belongs_question_id = %d
							AND belongs_question_type = %s
					",
					$question_id,
					esc_sql( $question_type )
				)
			);

			//phpcs:ignore Squiz.Operators.IncrementDecrementUsage.Found
			$next_order_id = $next_order_id + 1;

			if ( $question ) {
				if ( 'true_false' === $question_type ) {
					$wpdb->delete(
						$wpdb->prefix . 'tutor_quiz_question_answers',
						array(
							'belongs_question_id'   => $question_id,
							'belongs_question_type' => $question_type,
						)
					);
					$data_true_false = array(
						array(
							'belongs_question_id'   => esc_sql( $question_id ),
							'belongs_question_type' => $question_type,
							'answer_title'          => __( 'True', 'tutor' ),
							'is_correct'            => 'true' == $answer['true_false'] ? 1 : 0,
							'answer_two_gap_match'  => 'true',
						),
						array(
							'belongs_question_id'   => esc_sql( $question_id ),
							'belongs_question_type' => $question_type,
							'answer_title'          => __( 'False', 'tutor' ),
							'is_correct'            => 'false' === $answer['true_false'] ? 1 : 0,
							'answer_two_gap_match'  => 'false',
						),
					);

					foreach ( $data_true_false as $true_false_data ) {
						$wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $true_false_data );
					}
				} elseif ( 'multiple_choice' === $question_type ||
					'single_choice' === $question_type ||
					'ordering' === $question_type ||
					'matching' === $question_type ||
					'image_matching' === $question_type ||
					'image_answering' === $question_type ) {

					$answer_data = array(
						'belongs_question_id'   => sanitize_text_field( $question_id ),
						'belongs_question_type' => $question_type,
						'answer_title'          => sanitize_text_field( $answer['answer_title'] ),
						'image_id'              => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
						'answer_view_format'    => isset( $answer['answer_view_format'] ) ? $answer['answer_view_format'] : 0,
						'answer_order'          => $next_order_id,
					);
					if ( isset( $answer['matched_answer_title'] ) ) {
						$answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
					}

					$wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );

				} elseif ( 'fill_in_the_blank' === $question_type ) {
					$wpdb->delete(
						$wpdb->prefix . 'tutor_quiz_question_answers',
						array(
							'belongs_question_id'   => $question_id,
							'belongs_question_type' => $question_type,
						)
					);
					$answer_data = array(
						'belongs_question_id'   => sanitize_text_field( $question_id ),
						'belongs_question_type' => $question_type,
						'answer_title'          => sanitize_text_field( $answer['answer_title'] ),
						'answer_two_gap_match'  => isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null,
					);
					$wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data );
				}
			}
		}

		// Send response to browser if not internal call.
		if ( $response ) {
			wp_send_json_success();
			exit;
		}
	}

	/**
	 * Tutor Update Answer
	 *
	 * @since 1.0.0
	 *
	 * @return void send wp_json response
	 */
	public function tutor_update_quiz_answer_options() {
		tutor_utils()->checking_nonce();

		global $wpdb;

		$answer_id = Input::post( 'tutor_quiz_answer_id', 0, Input::TYPE_INT );

		if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
		}

		// Data sanitizing by helper method.
		$questions = tutor_sanitize_data( wp_unslash( $_POST['tutor_quiz_question'] ) ); //phpcs:ignore
		$answers   = tutor_sanitize_data( wp_unslash( $_POST['quiz_answer'] ) ); //phpcs:ignore

		foreach ( $answers as $question_id => $answer ) {
			$question      = tutor_utils()->avalue_dot( $question_id, $questions );
			$question_type = $question['question_type'];

			if ( $question ) {
				if ( 'multiple_choice' === $question_type ||
				'single_choice' === $question_type ||
				'ordering' === $question_type ||
				'matching' === $question_type ||
				'image_matching' === $question_type ||
				'fill_in_the_blank' === $question_type ||
				'image_answering' === $question_type ) {

					$answer_data = array(
						'belongs_question_id'   => $question_id,
						'belongs_question_type' => $question_type,
						'answer_title'          => sanitize_text_field( $answer['answer_title'] ),
						'image_id'              => isset( $answer['image_id'] ) ? $answer['image_id'] : 0,
						'answer_view_format'    => isset( $answer['answer_view_format'] ) ? sanitize_text_field( $answer['answer_view_format'] ) : '',
					);
					if ( isset( $answer['matched_answer_title'] ) ) {
						$answer_data['answer_two_gap_match'] = sanitize_text_field( $answer['matched_answer_title'] );
					}

					if ( 'fill_in_the_blank' === $question_type ) {
						$answer_data['answer_two_gap_match'] = isset( $answer['answer_two_gap_match'] ) ? sanitize_text_field( trim( $answer['answer_two_gap_match'] ) ) : null;
					}

					$wpdb->update( $wpdb->prefix . 'tutor_quiz_question_answers', $answer_data, array( 'answer_id' => $answer_id ) );
				}
			}
		}
		wp_send_json_success();
	}

	/**
	 * Get answers by quiz id
	 *
	 * @since 1.0.0
	 *
	 * @param int     $question_id question id.
	 * @param mixed   $question_type type of question.
	 * @param boolean $is_correct only correct answers or not.
	 *
	 * @return wpdb:get_results
	 */
	private function get_answers_by_q_id( $question_id, $question_type, $is_correct = false ) {
		global $wpdb;

		$correct_clause = $is_correct ? ' AND is_correct=1 ' : '';
		//phpcs:disable
		return $wpdb->get_results(
			$wpdb->prepare(
				"SELECT * FROM {$wpdb->prefix}tutor_quiz_question_answers
					WHERE belongs_question_id = %d 
						AND belongs_question_type = %s 
						{$correct_clause}
					ORDER BY answer_order ASC;
				",
				$question_id,
				esc_sql( $question_type )
			)
		);
		//phpcs:enable
	}

	/**
	 * Quiz builder changed type
	 *
	 * @since 1.0.0
	 *
	 * @return void send wp_json response
	 */
	public function tutor_quiz_builder_change_type() {
		tutor_utils()->checking_nonce();

		global $wpdb;
		$question_id   = Input::post( 'question_id', 0, Input::TYPE_INT );
		$question_type = Input::post( 'question_type' );

		if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
		}

		// Get question data by question ID.
		$question = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * 
					FROM {$wpdb->prefix}tutor_quiz_questions
					WHERE question_id = %d
				",
				$question_id
			)
		);

		// Get answers by question ID.
		$answers = $this->get_answers_by_q_id( $question_id, $question_type );

		ob_start();
		require tutor()->path . '/views/modal/question_answer_list.php';
		$output = ob_get_clean();

		wp_send_json_success( array( 'output' => $output ) );
	}


	/**
	 * Create quiz question
	 *
	 * @since 3.0.0
	 *
	 * @return void
	 */
	public function ajax_quiz_question_create() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );

		if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
			$this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
		}

		global $wpdb;
		$next_question_sl    = QueryHelper::get_count( $wpdb->prefix . 'tutor_quiz_questions', array( 'quiz_id' => $quiz_id ), array(), '*' ) + 1;
		$next_question_order = QuizModel::quiz_next_question_order_id( $quiz_id );
		$question_title      = __( 'Question', 'tutor' ) . ' ' . $next_question_sl;

		$new_question_data = array(
			'quiz_id'              => $quiz_id,
			'question_title'       => $question_title,
			'question_description' => '',
			'question_type'        => 'true_false',
			'question_mark'        => 1,
			'question_settings'    => maybe_serialize( array() ),
			'question_order'       => esc_sql( $next_question_order ),
		);

		$new_question_data = apply_filters( 'tutor_quiz_question_data', $new_question_data );

		$wpdb->insert( $wpdb->prefix . 'tutor_quiz_questions', $new_question_data );
		$question_id = $wpdb->insert_id;

		// Add question with default true_false type and options.
		$this->add_true_false_options( $question_id );

		// Add created question object to response.
		$question                   = QuizModel::get_question( $question_id );
		$question->question_answers = QuizModel::get_question_answers( $question->question_id );
		if ( isset( $question->question_settings ) ) {
			$question->question_settings = maybe_unserialize( $question->question_settings );
		}

		$this->json_response(
			__( 'Question created successfully', 'tutor' ),
			$question,
			HttpHelper::STATUS_CREATED
		);
	}

	/**
	 * Update question
	 *
	 * @since 3.0.0
	 *
	 * @return void
	 */
	public function ajax_quiz_question_update() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		global $wpdb;

		$question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
		if ( ! $question_id ) {
			$this->json_response( __( 'Invalid quiz question ID', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
			$this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
		}

		$requires_answers = array(
			'multiple_choice',
			'single_choice',
			'true_false',
			'fill_in_the_blank',
			'matching',
			'image_matching',
			'image_answering',
			'ordering',
		);

		$need_correct = array(
			'multiple_choice',
			'single_choice',
			'true_false',
		);

		$question_title    = Input::post( 'question_title', '' );
		$question_type     = Input::post( 'question_type', 'true_false' );
		$question_mark     = Input::post( 'question_mark', 1, Input::TYPE_INT );
		$question_settings    = Input::sanitize_array( $_POST['question_settings'] ?? array() ); //phpcs:ignore

		add_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );
		$question_description = Input::post( 'question_description', '', Input::TYPE_KSES_POST );
		remove_filter( 'wp_kses_allowed_html', Input::class . '::allow_iframe', 10, 2 );

		if ( in_array( $question_type, $requires_answers, true ) ) {
			$require_correct = in_array( $question_type, $need_correct, true );
			$all_answers     = $this->get_answers_by_q_id( $question_id, $question_type );
			$correct_answers = $this->get_answers_by_q_id( $question_id, $question_type, $require_correct );

			if ( ! empty( $all_answers ) && empty( $correct_answers ) ) {
				$this->json_response(
					__( 'Please make sure the question has answer', 'tutor' ),
					null,
					HttpHelper::STATUS_BAD_REQUEST
				);
			}
		}

		if ( isset( $question_settings['question_title'] ) ) {
			unset( $question_settings['question_title'] );
		}

		if ( isset( $question_settings['question_description'] ) ) {
			unset( $question_settings['question_description'] );
		}

		$data = array(
			'question_title'       => $question_title,
			'question_description' => $question_description,
			'question_type'        => $question_type,
			'question_mark'        => $question_mark,
			'question_settings'    => maybe_serialize( $question_settings ),
		);

		$data = apply_filters( 'tutor_quiz_question_data', $data );

		$wpdb->update( $wpdb->prefix . 'tutor_quiz_questions', $data, array( 'question_id' => $question_id ) );

		$this->json_response(
			__( 'Question updated successfully', 'tutor' ),
			$question_id
		);
	}

	/**
	 * Save quiz questions sorting
	 *
	 * @since 1.0.0
	 * @since 3.0.0 refactor and update response.
	 *
	 * @return void
	 */
	public function ajax_quiz_question_sorting() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		$quiz_id      = Input::post( 'quiz_id', 0, Input::TYPE_INT );
		$question_ids = Input::post( 'sorted_question_ids', array(), Input::TYPE_ARRAY );

		if ( ! tutor_utils()->can_user_manage( 'quiz', $quiz_id ) ) {
			$this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
		}

		global $wpdb;

		$i = 0;
		foreach ( $question_ids as $question_id ) {
			$i++;
			$wpdb->update(
				$wpdb->prefix . 'tutor_quiz_questions',
				array( 'question_order' => $i ),
				array(
					'quiz_id'     => $quiz_id,
					'question_id' => $question_id,
				)
			);
		}

		$this->json_response( __( 'Question order successfully updated', 'tutor' ) );
	}

	/**
	 * Add true false type question answer options.
	 *
	 * @param int $question_id question id.
	 *
	 * @return void
	 */
	private function add_true_false_options( $question_id ) {
		global $wpdb;
		$question_type = 'true_false';

		$wpdb->delete(
			$wpdb->prefix . 'tutor_quiz_question_answers',
			array(
				'belongs_question_id'   => $question_id,
				'belongs_question_type' => $question_type,
			)
		);

		$data = array(
			array(
				'belongs_question_id'   => $question_id,
				'belongs_question_type' => $question_type,
				'answer_title'          => __( 'True', 'tutor' ),
				'is_correct'            => 1,
				'answer_two_gap_match'  => 'true',
			),
			array(
				'belongs_question_id'   => $question_id,
				'belongs_question_type' => $question_type,
				'answer_title'          => __( 'False', 'tutor' ),
				'is_correct'            => 0,
				'answer_two_gap_match'  => 'false',
			),
		);

		foreach ( $data as $row ) {
			$wpdb->insert( $wpdb->prefix . 'tutor_quiz_question_answers', $row );
		}
	}

	/**
	 * Save question answer
	 *
	 * @since 3.0.0
	 *
	 * @return void
	 */
	public function ajax_quiz_question_answer_save() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		$is_update   = false;
		$question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
		$answer_id   = Input::post( 'answer_id', 0, Input::TYPE_INT );

		if ( $answer_id ) {
			$is_update = true;
		}

		if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
			$this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
		}

		global $wpdb;

		$table_question = "{$wpdb->prefix}tutor_quiz_questions";
		$table_answer   = "{$wpdb->prefix}tutor_quiz_question_answers";

		$question = QueryHelper::get_row( $table_question, array( 'question_id' => $question_id ), 'question_id' );

		if ( ! $question ) {
			$this->json_response(
				__( 'Invalid question', 'tutor' ),
				null,
				HttpHelper::STATUS_BAD_REQUEST
			);
		}

		$question_type      = Input::post( 'question_type' );
		$answer_title       = Input::post( 'answer_title', '' );
		$image_id           = Input::post( 'image_id', 0, Input::TYPE_INT );
		$answer_view_format = Input::post( 'answer_view_format', '' );

		$answer_data = array(
			'belongs_question_id'   => $question_id,
			'belongs_question_type' => $question_type,
			'answer_title'          => $answer_title,
		);

		if ( ! $is_update ) {
			$answer_data['answer_order'] = QuizModel::get_next_answer_order( $question_id, $question_type );
		}

		$question_types = array(
			'single_choice',
			'multiple_choice',
			'ordering',
			'matching',
			'image_matching',
			'image_answering',
		);

		if ( in_array( $question_type, $question_types, true ) ) {
			$answer_data['image_id']           = $image_id;
			$answer_data['answer_view_format'] = $answer_view_format;

			if ( Input::has( 'matched_answer_title' ) ) {
				$answer_data['answer_two_gap_match'] = Input::post( 'matched_answer_title' );
			}
		} elseif ( 'fill_in_the_blank' === $question_type ) {
			$answer_data['answer_two_gap_match'] = Input::post( 'answer_two_gap_match' );
		}

		if ( $is_update ) {
			$wpdb->update( $table_answer, $answer_data, array( 'answer_id' => $answer_id ) );
		} else {
			$question_types[] = 'fill_in_the_blank';
			if ( ! in_array( $question_type, $question_types, true ) ) {
				$this->json_response( __( 'Invalid question type', 'tutor' ), null, HttpHelper::STATUS_BAD_REQUEST );
			}

			$answer_data['belongs_question_type'] = Input::post( 'question_type' );
			$wpdb->insert( $table_answer, $answer_data );
			$answer_id = $wpdb->insert_id;
		}

		if ( $is_update ) {
			$this->json_response(
				__( 'Question answer updated successfully', 'tutor' ),
				$answer_id
			);
		} else {
			$this->json_response(
				__( 'Question answer saved successfully', 'tutor' ),
				$answer_id,
				HttpHelper::STATUS_CREATED
			);
		}
	}

	/**
	 * Delete quiz question's answer
	 *
	 * @since 3.0.0
	 *
	 * @return void
	 */
	public function ajax_quiz_question_answer_delete() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		$answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );

		if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
			$this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
		}

		global $wpdb;
		$wpdb->delete( $wpdb->prefix . 'tutor_quiz_question_answers', array( 'answer_id' => $answer_id ) );

		$this->json_response( __( 'Answer deleted successfully', 'tutor' ) );
	}

	/**
	 * Quiz question's answer shorting
	 *
	 * @since 1.0.0
	 * @since 3.0.0 refactor and response update.
	 *
	 * @return void
	 */
	public function ajax_quiz_question_answer_sorting() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		$question_id = Input::post( 'question_id', 0, Input::TYPE_INT );
		$answer_ids  = Input::post( 'sorted_answer_ids', array(), Input::TYPE_ARRAY );

		if ( ! tutor_utils()->can_user_manage( 'question', $question_id ) ) {
			$this->json_response( tutor_utils()->error_message(), null, HttpHelper::STATUS_FORBIDDEN );
		}

		global $wpdb;
		$i = 0;
		foreach ( $answer_ids as $answer_id ) {
			$i++;
			$wpdb->update(
				$wpdb->prefix . 'tutor_quiz_question_answers',
				array( 'answer_order' => $i ),
				array(
					'belongs_question_id' => $question_id,
					'answer_id'           => $answer_id,
				)
			);
		}

		$this->json_response( __( 'Question answer order successfully updated', 'tutor' ) );
	}

	/**
	 * Mark answer as correct
	 *
	 * @since 1.0.0
	 * @since 3.0.0 refactor and response updated.
	 *
	 * @return void
	 */
	public function ajax_mark_answer_as_correct() {
		if ( ! tutor_utils()->is_nonce_verified() ) {
			$this->json_response( tutor_utils()->error_message( 'nonce' ), null, HttpHelper::STATUS_BAD_REQUEST );
		}

		global $wpdb;

		$answer_id = Input::post( 'answer_id', 0, Input::TYPE_INT );

		if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
		}

		// get question info.
		$belong_question = $wpdb->get_row(
			$wpdb->prepare(
				" SELECT belongs_question_id, belongs_question_type
				FROM {$wpdb->tutor_quiz_question_answers}
				WHERE answer_id = %d
				LIMIT 1
			",
				$answer_id
			)
		);

		if ( $belong_question ) {
			// if question found update all answer is_correct to 0 except post answer.
			$question_type = $belong_question->belongs_question_type;
			$question_id   = $belong_question->belongs_question_id;
			if ( 'true_false' === $question_type || 'single_choice' === $question_type ) {
				$update = $wpdb->query(
					$wpdb->prepare(
						"UPDATE {$wpdb->tutor_quiz_question_answers}
						SET is_correct = 0
						WHERE belongs_question_id = %d
							AND answer_id != %d
					",
						$question_id,
						$answer_id
					)
				);
			}
		}

		$is_correct = Input::post( 'is_correct', 0, Input::TYPE_INT );

		if ( ! tutor_utils()->can_user_manage( 'quiz_answer', $answer_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied', 'tutor' ) ) );
		}

		$answer = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT * 
					FROM {$wpdb->prefix}tutor_quiz_question_answers 
					WHERE answer_id = %d 
					LIMIT 0,1 ;
				",
				$answer_id
			)
		);

		if ( 'single_choice' === $answer->belongs_question_type ) {
			$wpdb->update(
				$wpdb->prefix . 'tutor_quiz_question_answers',
				array( 'is_correct' => 0 ),
				array( 'belongs_question_id' => esc_sql( $answer->belongs_question_id ) )
			);
		}

		$wpdb->update(
			$wpdb->prefix . 'tutor_quiz_question_answers',
			array( 'is_correct' => $is_correct ),
			array( 'answer_id' => $answer_id )
		);

		$this->json_response(
			__( 'Answer mark as correct updated', 'tutor' ),
			$answer_id
		);
	}

	/**
	 * Rendering quiz for frontend
	 *
	 * @since 1.0.0
	 *
	 * @return void send wp_json response
	 */
	public function tutor_render_quiz_content() {

		tutor_utils()->checking_nonce();

		$quiz_id = Input::post( 'quiz_id', 0, Input::TYPE_INT );

		if ( ! tutor_utils()->has_enrolled_content_access( 'quiz', $quiz_id ) ) {
			wp_send_json_error( array( 'message' => __( 'Access Denied.', 'tutor' ) ) );
		}

		ob_start();
		global $post;

		$post = get_post( $quiz_id ); //phpcs:ignore
		setup_postdata( $post );

		single_quiz_contents();
		wp_reset_postdata();

		$html = ob_get_clean();
		wp_send_json_success( array( 'html' => $html ) );
	}

	/**
	 * Get attempt details
	 *
	 * @since 1.0.0
	 *
	 * @param int $attempt_id required attempt id to get details.
	 *
	 * @return mixed object on success, null on failure
	 */
	public static function attempt_details( int $attempt_id ) {
		global $wpdb;
		$attempt_details = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT *
					FROM {$wpdb->prefix}tutor_quiz_attempts
					WHERE attempt_id = %d
				",
				$attempt_id
			)
		);
		return $attempt_details;
	}

	/**
	 * Update quiz attempt info
	 *
	 * @since 1.0.0
	 *
	 * @param int   $attempt_id attempt id.
	 * @param mixed $attempt_info serialize data.
	 *
	 * @return bool, true on success, false on failure
	 */
	public static function update_attempt_info( int $attempt_id, $attempt_info ) {
		global $wpdb;
		$table       = $wpdb->prefix . 'tutor_quiz_attempts';
		$update_info = $wpdb->update(
			$table,
			array( 'attempt_info' => $attempt_info ),
			array( 'attempt_id' => $attempt_id )
		);
		return $update_info ? true : false;
	}

	/**
	 * Attempt delete ajax request handler
	 *
	 * @since 2.1.0
	 *
	 * @return void  wp_json response
	 */
	public function attempt_delete() {
		tutor_utils()->checking_nonce();

		$attempt_id = Input::post( 'id', 0, Input::TYPE_INT );
		$attempt    = tutor_utils()->get_attempt( $attempt_id );
		if ( ! $attempt ) {
			wp_send_json_error( __( 'Invalid attempt ID', 'tutor' ) );
		}

		$user_id   = get_current_user_id();
		$course_id = $attempt->course_id;

		if ( tutor_utils()->can_user_edit_course( $user_id, $course_id ) ) {
			QuizModel::delete_quiz_attempt( $attempt_id );
			wp_send_json_success( __( 'Attempt deleted successfully!', 'tutor' ) );
		} else {
			wp_send_json_error( tutor_utils()->error_message() );
		}
	}

}

Zerion Mini Shell 1.0