Source: includes/class-sensei-question.php

<?php
if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly
}

/**
 * Sensei Question Class
 *
 * All functionality pertaining to the questions post type in Sensei.
 *
 * @package Assessment
 * @author Automattic
 *
 * @since 1.0.0
 */
class Sensei_Question {
	/**
	 * Question token.
	 *
	 * @var string
	 */
	public $token;

	/**
	 * Question meta fields.
	 *
	 * @var array
	 */
	public $meta_fields;

	/**
	 * Constructor.
	 *
	 * @since  1.0.0
	 */
	public function __construct() {
		$this->token       = 'question';
		$this->meta_fields = array( 'question_right_answer', 'question_wrong_answers' );

		if ( is_admin() ) {
			// Custom Write Panel Columns
			add_filter( 'manage_edit-question_columns', array( $this, 'add_column_headings' ), 20, 1 );
			add_action( 'manage_posts_custom_column', array( $this, 'add_column_data' ), 10, 2 );
			add_action( 'add_meta_boxes', array( $this, 'question_edit_panel_metabox' ), 10, 2 );

			// Question list table filters
			add_action( 'restrict_manage_posts', array( $this, 'filter_options' ) );
			add_filter( 'request', array( $this, 'filter_actions' ) );

			add_action( 'save_post_question', array( $this, 'save_question' ), 10, 1 );

			// Add custom navigation.
			add_action( 'in_admin_header', [ $this, 'add_custom_navigation' ] );
		}

		add_action( 'sensei_question_initial_publish', [ $this, 'log_initial_publish_event' ] );
	}

	/**
	 * Add custom navigation to the admin pages.
	 *
	 * @since 4.0.0
	 * @access private
	 */
	public function add_custom_navigation() {
		$screen = get_current_screen();

		if ( ! $screen ) {
			return;
		}

		if ( in_array( $screen->id, [ 'edit-question', 'edit-question-category' ], true ) && ( 'term' !== $screen->base ) ) {
			$this->display_question_navigation( $screen );
		}
	}

	/**
	 * Highlight the menu item for the question pages.
	 *
	 * @deprecated 4.8.0
	 *
	 * @since 4.0.0
	 * @access private
	 *
	 * @param string $submenu_file The submenu file points to the certain item of the submenu.
	 *
	 * @return string
	 */
	public function highlight_menu_item( $submenu_file ) {
		_deprecated_function( __METHOD__, '4.8.0' );

		$screen = get_current_screen();

		if ( $screen && 'edit-question-category' === $screen->id ) {
			$submenu_file = 'edit.php?post_type=question';
		}

		return $submenu_file;
	}

	/**
	 * Display the lessons' navigation.
	 *
	 * @param WP_Screen $screen
	 */
	private function display_question_navigation( WP_Screen $screen ) {
		?>
		<div id="sensei-custom-navigation" class="sensei-custom-navigation">
			<div class="sensei-custom-navigation__heading">
				<div class="sensei-custom-navigation__title">
					<h1><?php esc_html_e( 'Questions ', 'sensei-lms' ); ?></h1>
				</div>
				<div class="sensei-custom-navigation__links">
					<a class="page-title-action" href="<?php echo esc_url( admin_url( 'post-new.php?post_type=question' ) ); ?>"><?php esc_html_e( 'New Question', 'sensei-lms' ); ?></a>
				</div>
			</div>
			<div class="sensei-custom-navigation__tabbar">
				<a class="sensei-custom-navigation__tab <?php echo '' === $screen->taxonomy ? 'active' : ''; ?>" href="<?php echo esc_url( admin_url( 'edit.php?post_type=question' ) ); ?>"><?php esc_html_e( 'All Questions', 'sensei-lms' ); ?></a>
				<a class="sensei-custom-navigation__tab <?php echo 'question-category' === $screen->taxonomy ? 'active' : ''; ?>" href="<?php echo esc_url( admin_url( 'edit-tags.php?taxonomy=question-category&post_type=question' ) ); ?>"><?php esc_html_e( 'Question Categories', 'sensei-lms' ); ?></a>
			</div>
		</div>
		<?php
	}

	public function question_types() {
		$types = array(
			'multiple-choice' => __( 'Multiple Choice', 'sensei-lms' ),
			'boolean'         => __( 'True/False', 'sensei-lms' ),
			'gap-fill'        => __( 'Gap Fill', 'sensei-lms' ),
			'single-line'     => __( 'Single Line', 'sensei-lms' ),
			'multi-line'      => __( 'Multi Line', 'sensei-lms' ),
			'file-upload'     => __( 'File Upload', 'sensei-lms' ),
		);

		/**
		 * Filter the question types.
		 *
		 * @hook sensei_question_types
		 *
		 * @param {string[]} $types Question types.
		 * @return {string[]} Associative array of question types.
		 */
		return apply_filters( 'sensei_question_types', $types );
	}

	/**
	 * Add column headings to the "question" post list screen,
	 * while moving the existing ones to the end.
	 *
	 * @access private
	 * @since  1.3.0
	 * @param  array $defaults  Array of column header labels keyed by column ID.
	 * @return array            Updated array of column header labels keyed by column ID.
	 */
	public function add_column_headings( $defaults ) {
		$new_columns                      = [];
		$new_columns['cb']                = '<input type="checkbox" />';
		$new_columns['title']             = _x( 'Question', 'column name', 'sensei-lms' );
		$new_columns['question-type']     = _x( 'Type', 'column name', 'sensei-lms' );
		$new_columns['question-category'] = _x( 'Categories', 'column name', 'sensei-lms' );
		if ( isset( $defaults['date'] ) ) {
			$new_columns['date'] = $defaults['date'];
		}

		// Unset renamed existing columns.
		unset( $defaults['taxonomy-question-type'] );
		unset( $defaults['taxonomy-question-category'] );

		// Add all remaining columns at the end.
		foreach ( $defaults as $column_key => $column_value ) {
			if ( ! isset( $new_columns[ $column_key ] ) ) {
				$new_columns[ $column_key ] = $column_value;
			}
		}

		return $new_columns;
	}


	/**
	 * Add data for our newly-added custom columns.
	 *
	 * @access public
	 * @since  1.3.0
	 * @param  string $column_name
	 * @param  int    $id
	 * @return void
	 */
	public function add_column_data( $column_name, $id ) {
		switch ( $column_name ) {

			case 'id':
				echo esc_html( $id );
				break;

			case 'question-type':
				$question_types = $this->question_types();
				$question_type  = strip_tags( get_the_term_list( $id, 'question-type', '', ', ', '' ) );
				$output         = '&mdash;';

				if ( isset( $question_types[ $question_type ] ) ) {
					$output = $question_types[ $question_type ];
				}

				echo esc_html( $output );
				break;

			case 'question-category':
				$output = strip_tags( get_the_term_list( $id, 'question-category', '', ', ', '' ) );
				if ( ! $output ) {
					$output = '&mdash;';
				}
				echo esc_html( $output );
				break;

			default:
				break;

		}

	}

	public function question_edit_panel_metabox( $post_type, $post ) {
		if ( in_array( $post_type, array( 'question', 'multiple_question' ) ) ) {

			$metabox_title = __( 'Question', 'sensei-lms' );

			if ( isset( $post->ID ) ) {

				$question_type = Sensei()->question->get_question_type( $post->ID );

				if ( $question_type ) {
					$question_types = $this->question_types();
					$type           = $question_types[ $question_type ];

					if ( $type ) {
						$metabox_title = $type;
					}
				}
			}

			add_meta_box( 'question-lessons-panel', __( 'Quizzes', 'sensei-lms' ), array( $this, 'question_lessons_panel' ), 'question', 'side', 'default' );

			if ( ! Sensei()->quiz->is_block_based_editor_enabled() ) {
				add_meta_box( 'multiple-question-lessons-panel', __( 'Quizzes', 'sensei-lms' ), array( $this, 'question_lessons_panel' ), 'multiple_question', 'side', 'default' );
				add_meta_box( 'question-edit-panel', $metabox_title, array( $this, 'question_edit_panel' ), 'question', 'normal', 'high' );
				add_filter( 'sensei_scripts_allowed_post_types', [ $this, 'load_lesson_edit_script' ] );
			}
		}
	}

	/**
	 * Also load the lesson metabox scripts for the question post type when using the legacy editor.
	 *
	 * @access private
	 *
	 * @param array $post_types Post types.
	 *
	 * @return array
	 */
	public function load_lesson_edit_script( $post_types ) {
		$post_types[] = 'question';
		return $post_types;
	}

	public function question_edit_panel() {
		global  $post, $pagenow;

		if ( Sensei()->quiz->is_block_based_editor_enabled() ) {
			return;
		}

		add_action( 'admin_enqueue_scripts', array( Sensei()->lesson, 'enqueue_scripts' ) );
		add_action( 'admin_enqueue_scripts', array( Sensei()->lesson, 'enqueue_styles' ) );

		$html = '<div id="lesson-quiz" class="single-question"><div id="add-question-main">';

		if ( 'post-new.php' == $pagenow ) {

			$html .= '<div id="add-question-actions">';
			$html .= Sensei()->lesson->quiz_panel_add( 'question' );
			$html .= '</div>';

		} else {
			$question_id = $post->ID;

			$question_type = Sensei()->question->get_question_type( $post->ID );

			$html .= '<div id="add-question-metadata"><table class="widefat">';
			$html .= Sensei()->lesson->quiz_panel_question( $question_type, 0, $question_id, 'question' );
			$html .= '</table></div>';
		}

		$html .= '</div></div>';

		echo wp_kses(
			$html,
			array_merge(
				wp_kses_allowed_html( 'post' ),
				array(
					'button'   => array(
						'class'                     => array(),
						'data-uploader-button-text' => array(),
						'data-uploader-title'       => array(),
						'id'                        => array(),
					),
					'input'    => array(
						'checked'     => array(),
						'class'       => array(),
						'id'          => array(),
						'max'         => array(),
						'min'         => array(),
						'name'        => array(),
						'placeholder' => array(),
						'rel'         => array(),
						'size'        => array(),
						'type'        => array(),
						'value'       => array(),
					),
					// Explicitly allow label tag for WP.com.
					'label'    => array(
						'class' => array(),
						'for'   => array(),
					),
					'option'   => array(
						'value' => array(),
					),
					'select'   => array(
						'class' => array(),
						'id'    => array(),
						'name'  => array(),
					),
					// Explicitly allow textarea tag for WP.com.
					'textarea' => array(
						'class' => array(),
						'id'    => array(),
						'name'  => array(),
						'rows'  => array(),
					),
				)
			)
		);
	}

	public function question_lessons_panel() {
		global $post;

		// translators: Placeholders are an opening and closing <em> tag.
		$no_lessons = sprintf( __( '%1$sThis question does not appear in any quizzes yet.%2$s', 'sensei-lms' ), '<em>', '</em>' );

		if ( ! isset( $post->ID ) ) {
			echo wp_kses_post( $no_lessons );
			return;
		}

		// This retrieves those quizzes the question is directly connected to.
		$quizzes = get_post_meta( $post->ID, '_quiz_id', false );

		// Collate all 'multiple_question' quizzes the question is part of.
		$categories_of_question = wp_get_post_terms( $post->ID, 'question-category', array( 'fields' => 'ids' ) );
		if ( ! empty( $categories_of_question ) ) {
			foreach ( $categories_of_question as $term_id ) {
				$qargs            = array(
					'fields'           => 'ids',
					'post_type'        => 'multiple_question',
					'posts_per_page'   => -1,
					'meta_query'       => array(
						array(
							'key'   => 'category',
							'value' => $term_id,
						),
					),
					'post_status'      => 'any',
					'suppress_filters' => 0,
				);
				$cat_question_ids = get_posts( $qargs );
				foreach ( $cat_question_ids as $cat_question_id ) {
					$cat_quizzes = get_post_meta( $cat_question_id, '_quiz_id', false );
					$quizzes     = array_merge( $quizzes, $cat_quizzes );
				}
			}
			$quizzes = array_unique( array_filter( $quizzes ) );
		}

		if ( ! $quizzes ) {
			echo wp_kses_post( $no_lessons );
			return;
		}

		$lessons = [];

		foreach ( $quizzes as $quiz ) {

			$lesson_id = get_post_meta( $quiz, '_quiz_lesson', true );

			if ( ! $lesson_id ) {
				continue;
			}

			$lessons[ $lesson_id ]['title'] = get_the_title( $lesson_id );
			$lessons[ $lesson_id ]['link']  = admin_url( 'post.php?post=' . $lesson_id . '&action=edit' );
		}

		if ( ! $lessons ) {
			echo wp_kses_post( $no_lessons );
			return;
		}

		$html = '<ul>';

		foreach ( $lessons as $id => $lesson ) {
			$html .= '<li><a href="' . esc_url( $lesson['link'] ) . '">' . esc_html( $lesson['title'] ) . '</a></li>';
		}

		$html .= '</ul>';

		echo wp_kses_post( $html );

	}

	public function save_question( $post_id = 0 ) {
		/*
		 * Ensure that we are on the `post` screen. If so, we can trust that
		 * nonce verification has been performed.
		 */
		$screen = get_current_screen();
		if ( ! $screen || 'post' !== $screen->base ) {
			return;
		}

		// Setup the data for saving.
		// phpcs:ignore WordPress.Security.NonceVerification
		$data                = $_POST;
		$data['quiz_id']     = 0;
		$data['question_id'] = $post_id;

		if ( ! wp_is_post_revision( $post_id ) ) {

			// Unhook function to prevent infinite loops
			remove_action( 'save_post_question', array( $this, 'save_question' ) );

			// Update question data
			Sensei()->lesson->lesson_save_question( $data, 'question' );

			// Re-hook same function
			add_action( 'save_post_question', array( $this, 'save_question' ) );
		}

		return;
	}

	/**
	 * Add options to filter the questions list table
	 *
	 * @return void
	 */
	public function filter_options() {
		global $typenow;

		if ( is_admin() && 'question' == $typenow ) {

			$output = '';

			// Question type
			$selected       = isset( $_GET['question_type'] ) ? $_GET['question_type'] : '';
			$type_options   = '<option value="">' . esc_html__( 'All types', 'sensei-lms' ) . '</option>';
			$question_types = $this->question_types();

			foreach ( $question_types as $label => $type ) {
				$type_options .= '<option value="' . esc_attr( $label ) . '" ' . selected( $selected, $label, false ) . '>' . esc_html( $type ) . '</option>';
			}

			$output .= '<select name="question_type" id="dropdown_question_type">';
			$output .= $type_options;
			$output .= '</select>';

			// Question category
			$cats = get_terms( 'question-category', array( 'hide_empty' => false ) );
			if ( ! empty( $cats ) && ! is_wp_error( $cats ) ) {
				$selected    = isset( $_GET['question_cat'] ) ? $_GET['question_cat'] : '';
				$cat_options = '<option value="">' . esc_html__( 'All categories', 'sensei-lms' ) . '</option>';
				foreach ( $cats as $cat ) {
					$cat_options .= '<option value="' . esc_attr( $cat->slug ) . '" ' . selected( $selected, $cat->slug, false ) . '>' . esc_html( $cat->name ) . '</option>';
				}

				$output .= '<select name="question_cat" id="dropdown_question_cat">';
				$output .= $cat_options;
				$output .= '</select>';
			}

			$allowed_html = array(
				'option' => array(
					'selected' => array(),
					'value'    => array(),
				),
				'select' => array(
					'id'   => array(),
					'name' => array(),
				),
			);

			echo wp_kses( $output, $allowed_html );
		}
	}

	/**
	 * Filter questions list table
	 *
	 * @param  array $request Current request
	 * @return array          Modified request
	 */
	public function filter_actions( $request ) {
		global $typenow;

		if ( is_admin() && 'question' == $typenow ) {

			// Question type
			$question_type = isset( $_GET['question_type'] ) ? $_GET['question_type'] : '';
			if ( $question_type ) {
				$type_query             = array(
					'taxonomy' => 'question-type',
					'terms'    => $question_type,
					'field'    => 'slug',
				);
				$request['tax_query'][] = $type_query;
			}

			// Question category
			$question_cat = isset( $_GET['question_cat'] ) ? $_GET['question_cat'] : '';
			if ( $question_cat ) {
				$cat_query              = array(
					'taxonomy' => 'question-category',
					'terms'    => $question_cat,
					'field'    => 'slug',
				);
				$request['tax_query'][] = $cat_query;
			}
		}

		return $request;
	}

	/**
	 * Get the type of question by id
	 *
	 * This function uses the post terms to determine which question type
	 * the passed question id belongs to.
	 *
	 * @since 1.7.4
	 *
	 * @param int $question_id
	 *
	 * @return string $question_type | bool
	 */
	public function get_question_type( $question_id ) {

		if ( empty( $question_id ) || ! intval( $question_id ) > 0
			|| 'question' != get_post_type( $question_id ) ) {
			return false;
		}

		$question_type  = 'multiple-choice';
		$question_types = wp_get_post_terms( $question_id, 'question-type' );

		foreach ( $question_types as $type ) {
			$question_type = $type->slug;
		}

		return $question_type;

	}

	/**
	 * Given a question ID, return the grade that can be achieved.
	 *
	 * @since 1.9
	 *
	 * @param int $question_id
	 *
	 * @return int $question_grade | bool
	 */
	public function get_question_grade( $question_id ) {

		if ( empty( $question_id ) || ! intval( $question_id ) > 0
			|| 'question' != get_post_type( $question_id ) ) {
			return false;
		}

		$question_grade_raw = get_post_meta( $question_id, '_question_grade', true );
		// If not set then default to 1...
		if ( false === $question_grade_raw || $question_grade_raw == '' ) {
			$question_grade = 1;
		}
		// ...but allow a grade of 0 for non-marked questions
		else {
			$question_grade = intval( $question_grade_raw );
		}

		/**
		 * Filter the grade for the given question.
		 *
		 * @since 1.9.6
		 *
		 * @hook sensei_get_question_grade
		 *
		 * @param {int} $question_grade Question grade.
		 * @param {int} $question_id    Question ID.
		 * @return {int} Question grade.
		 */
		return apply_filters( 'sensei_get_question_grade', $question_grade, $question_id );
	}


	/**
	 * This function simply loads the question type template
	 *
	 * @since 1.9.0
	 * @param string $question_type The question type.
	 */
	public static function load_question_template( $question_type ) {
		$old_template_name = 'single-quiz/question_type-' . $question_type . '.php';
		$new_template_name = 'single-quiz/question-type-' . $question_type . '.php';

		/*
		 * For backwards compatibility, try to locate and load the file with the
		 * old name first.
		 */
		if ( Sensei_Templates::locate_template( $old_template_name ) ) {
			Sensei_Templates::get_template( $old_template_name );
		} else {
			Sensei_Templates::get_template( $new_template_name );
		}
	}

	/**
	 * Echo the sensei question title.
	 *
	 * @uses Sensei_Question::get_the_question_title
	 *
	 * @since 1.9.0
	 * @param $question_id
	 */
	public static function the_question_title( $question_id ) {
		echo wp_kses_post( self::get_the_question_title( $question_id ) );
	}

	/**
	 * Generate the question title with it's grade.
	 *
	 * @since 1.9.0
	 *
	 * @param $question_id
	 * @return string
	 */
	public static function get_the_question_title( $question_id ) {
		/**
		 * Filter the question title.
		 *
		 * @since 1.3.0
		 *
		 * @hook sensei_question_title
		 *
		 * @param {string} $title Question title.
		 * @return {string} Question title.
		 */
		$title = apply_filters( 'sensei_question_title', get_the_title( $question_id ) );

		/**
		 * Filter Sensei single title
		 *
		 * @hook sensei_single_title
		 *
		 * @param {string} $title     The title.
		 * @param {string} $post_type The post type.
		 * @return {string} Filtered title.
		 */
		$title = apply_filters( 'sensei_single_title', $title, 'question' );

		$question_grade = Sensei()->question->get_question_grade( $question_id );

		$title_html = '<div class="sensei-lms-question-block__header"><h2 class="question question-title">';

		// translators: %d is the question number.
		$title_html .= sprintf( esc_html__( '%d. ', 'sensei-lms' ), sensei_get_the_question_number() );
		$title_html .= esc_html( $title );
		$title_html .= '</h2>';

		if ( $question_grade > 0 ) {
			$title_html .= Sensei()->view_helper->format_question_points( $question_grade );
		}

		$title_html .= '</div>';

		return $title_html;
	}

	/**
	 * Tech the question description
	 *
	 * @param $question_id
	 * @return string
	 */
	public static function get_the_question_description( $question_id ) {

		$question             = get_post( $question_id );
		$question_description = $question->post_content;

		if ( has_blocks( $question_description ) ) {
			$blocks = parse_blocks( $question_description );

			foreach ( $blocks as $block ) {
				if ( 'sensei-lms/question-description' === $block['blockName'] ) {
					$question_description = render_block( $block );
				}
			}
		}

		if ( ! empty( $question_description ) ) {
			$question_description = '<div class="wp-block-sensei-lms-question-description">' . $question_description . '</div>';
		}

		/**
		 * Already documented within WordPress Core
		 */
		return apply_filters( 'the_content', wp_kses_post( $question_description ) );
	}

	/**
	 * Output the question description
	 *
	 * @since 1.9.0
	 * @param $question_id
	 */
	public static function the_question_description( $question_id ) {
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Output escaped in called method (before `the_content` filter).
		echo self::get_the_question_description( $question_id );
	}

	/**
	 * Get the questions media markup
	 *
	 * @since 1.9.0
	 * @param $question_id
	 * @return string
	 */
	public static function get_the_question_media( $question_id ) {

		$question_media      = get_post_meta( $question_id, '_question_media', true );
		$question_media_link = '';
		if ( 0 < intval( $question_media ) ) {
			$mimetype = get_post_mime_type( $question_media );
			if ( $mimetype ) {
				$mimetype_array = explode( '/', $mimetype );
				if ( isset( $mimetype_array[0] ) && $mimetype_array[0] ) {
					$question_media_type        = $mimetype_array[0];
					$question_media_url         = wp_get_attachment_url( $question_media );
					$attachment                 = get_post( $question_media );
					$question_media_title       = $attachment->post_title;
					$question_media_description = $attachment->post_content;
					switch ( $question_media_type ) {
						case 'image':
							/**
							 * Filter the size of the question image.
							 *
							 * @hook sensei_question_image_size
							 *
							 * @param {string} $size        Image size.
							 * @param {int}    $question_id Question ID.
							 * @return {string} Image size.
							 */
							$image_size          = apply_filters( 'sensei_question_image_size', 'medium', $question_id );
							$attachment_src      = wp_get_attachment_image_src( $question_media, $image_size );
							$question_media_link = '<a class="' . esc_attr( $question_media_type ) . '" title="' . esc_attr( $question_media_title ) . '" href="' . esc_url( $question_media_url ) . '" target="_blank"><img src="' . esc_url( $attachment_src[0] ) . '" width="' . esc_attr( $attachment_src[1] ) . '" height="' . esc_attr( $attachment_src[2] ) . '" /></a>';
							break;

						case 'audio':
							$question_media_link = wp_audio_shortcode( array( 'src' => $question_media_url ) );
							break;

						case 'video':
							$question_media_link = wp_video_shortcode( array( 'src' => $question_media_url ) );
							break;

						default:
							$question_media_filename = basename( $question_media_url );
							$question_media_link     = '<a class="' . esc_attr( $question_media_type ) . '" title="' . esc_attr( $question_media_title ) . '" href="' . esc_url( $question_media_url ) . '" target="_blank">' . esc_html( $question_media_filename ) . '</a>';
							break;
					}
				}
			}
		}

		$output = '';
		if ( $question_media_link ) {

				$output .= '<div class="question_media_display">';
				$output .= self::question_media_kses( $question_media_link );
				$output .= '<dl>';

			if ( ! empty( $question_media_title ) ) {

				$output .= '<dt>' . wp_kses_post( $question_media_title ) . '</dt>';

			}

			if ( ! empty( $question_media_description ) ) {

				$output .= '<dd>' . wp_kses_post( $question_media_description ) . '</dd>';

			}

				$output .= '</dl>';
				$output .= '</div>';

		}

		return $output;

	}

	/**
	 * Output the question media
	 *
	 * @since 1.9.0
	 * @param string $question_id
	 */
	public static function the_question_media( $question_id ) {
		// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
		echo self::question_media_kses( self::get_the_question_media( $question_id ) );
	}


	/**
	 * Return the answer feedback CSS classes (default and custom) based if the answer is correct or not
	 *
	 * @param int  $question_id Question id.
	 * @param bool $answer_correct Flag indicating if the answer is correct or not.
	 * @return array CSS classes
	 */
	private static function get_answer_feedback_classes( $question_id, bool $answer_correct ): array {
		if ( $answer_correct ) {
			$feedback_block = Sensei_Quiz::get_correct_answer_feedback_block( $question_id );

			return [
				'sensei-lms-question__answer-feedback--correct',
				isset( $feedback_block['attrs']['className'] ) ? $feedback_block['attrs']['className'] : '',
			];

		} else {
			$feedback_block = Sensei_Quiz::get_incorrect_answer_feedback_block( $question_id );
			return [
				'sensei-lms-question__answer-feedback--incorrect',
				isset( $feedback_block['attrs']['className'] ) ? $feedback_block['attrs']['className'] : '',

			];
		}
	}

	/**
	 * Special kses processing for media output to allow 'source' video tag.
	 *
	 * @since 3.0.0
	 * @param string $source_string Source string.
	 * @return string with allowed html elements
	 */
	private static function question_media_kses( $source_string ) {
		$source_tag   = array(
			'source' => array(
				'type' => true,
				'src'  => true,
			),
		);
		$allowed_html = array_merge( $source_tag, wp_kses_allowed_html( 'post' ) );

		return wp_kses( $source_string, $allowed_html );
	}

	/**
	 * Output a special field for the question needed for question submission.
	 *
	 * @since      1.9.0
	 * @deprecated 3.15.0 use Sensei_Quiz::output_quiz_hidden_fields
	 *
	 * @param $question_id
	 */
	public static function the_question_hidden_fields( $question_id ) {

		// To be removed in 5.0.0.
		_deprecated_function( __METHOD__, '3.15.0', 'Sensei_Quiz::output_quiz_hidden_fields' );

		?>
			<input type="hidden" name="question_id_<?php echo esc_attr( $question_id ); ?>" value="<?php echo esc_attr( $question_id ); ?>" />
			<input type="hidden" name="questions_asked[]" value="<?php echo esc_attr( $question_id ); ?>" />
		<?php

	}

	/**
	 * Answer feedback (including correctness, grade, right answer and feedback notes) for a question.
	 *
	 * @since 3.14.0
	 *
	 * @param $question_id
	 */
	public static function the_answer_feedback( $question_id ) {

		$hide_answer_feedback = get_post_meta( $question_id, '_hide_answer_feedback', true );
		if ( $hide_answer_feedback ) {
			return;
		}

		$quiz_id   = get_the_ID();
		$lesson_id = Sensei()->quiz->get_lesson_id( $quiz_id );

		// Make sure this user has submitted answers before we show anything
		$user_answers = Sensei()->quiz->get_user_answers( $lesson_id, get_current_user_id() );
		if ( empty( $user_answers ) ) {
			return;
		}

		$user_quiz_progress = Sensei()->quiz_progress_repository->get( $quiz_id, get_current_user_id() );
		$user_quiz_grade    = Sensei_Quiz::get_user_quiz_grade( $lesson_id, get_current_user_id() );
		$reset_quiz_allowed = Sensei_Quiz::is_reset_allowed( $lesson_id );
		$quiz_graded        = $user_quiz_progress && ! in_array( $user_quiz_progress->get_status(), array( 'ungraded', 'in-progress' ) );

		$quiz_required_pass_grade = intval( get_post_meta( $quiz_id, '_quiz_passmark', true ) );
		$succeeded                = ! Sensei_Quiz::is_pass_required( $lesson_id ) || $user_quiz_grade >= $quiz_required_pass_grade;

		if ( ! $quiz_graded ) {
			return;
		}

		$default = ! $reset_quiz_allowed;

		// Explicit gradual feedback options.
		$indicate_incorrect   = $succeeded || Sensei_Quiz::get_option( $lesson_id, 'failed_indicate_incorrect', $default );
		$show_correct_answers = $succeeded || Sensei_Quiz::get_option( $lesson_id, 'failed_show_correct_answers', $default );
		$show_feedback_notes  = $succeeded || Sensei_Quiz::get_option( $lesson_id, 'failed_show_answer_feedback', $default );

		/**
		 * Allow dynamic overriding of whether to show question answers or not
		 *
		 * @since 1.9.7
		 *
		 * @hook  sensei_question_show_answers
		 *
		 * @param {bool} $show_answers Whether to show the answer to the question.
		 * @param {int}  $question_id  Question ID.
		 * @param {int}  $quiz_id      Quiz ID.
		 * @param {int}  $lesson_id    Lesson ID.
		 * @param {int}  $user_id      User ID.
		 * @return {bool} Whether to show the answer to the question.
		 */
		$show_correct_answers = apply_filters( 'sensei_question_show_answers', $show_correct_answers, $question_id, $quiz_id, $lesson_id, get_current_user_id() );

		$answer_grade   = (int) Sensei()->quiz->get_user_question_grade( $lesson_id, $question_id, get_current_user_id() );
		$answer_correct = $answer_grade > 0;

		$answer_notes_classnames = [];
		$answer_feedback_title   = '';

		if ( $indicate_incorrect ) {
			$answer_notes_classnames = self::get_answer_feedback_classes( $question_id, $answer_correct );
			if ( $answer_correct ) {
				$answer_feedback_title = __( 'Correct', 'sensei-lms' );
			} else {
				$answer_feedback_title = __( 'Incorrect', 'sensei-lms' );
			}
		}

		/**
		 * Filter the answer message CSS classes.
		 *
		 * @since  1.9.0
		 *
		 * @hook  sensei_question_answer_message_css_class
		 *
		 * @param {string} $answer_notes_classname Space-separated CSS classes to apply to answer message.
		 * @param {int}    $lesson_id              Lesson ID.
		 * @param {int}    $question_id            Question ID.
		 * @param {int}    $user_id                User ID.
		 * @param {bool}   $answer_correct         Whether this is the correct answer.
		 * @return {string} Space-separated CSS classes to apply to answer message.
		 */
		$answer_notes_classnames = apply_filters( 'sensei_question_answer_message_css_class', $answer_notes_classnames, $lesson_id, $question_id, get_current_user_id(), $answer_correct );

		$answer_notes = $show_feedback_notes ? Sensei()->quiz->get_user_question_feedback( $lesson_id, $question_id, get_current_user_id() ) : null;
		/**
		 * Filter the answer feedback.
		 *
		 * @since  1.9.0
		 *
		 * @hook   sensei_question_answer_notes
		 *
		 * @param  {bool|string} $answer_notes Answer notes.
		 * @param  {int}         $question_id  Question ID.
		 * @param  {int}         $lesson_id    Lesson ID.
		 * @return {string} Answer notes.
		 */
		$answer_notes = apply_filters( 'sensei_question_answer_notes', $answer_notes, $question_id, $lesson_id );

		$question_grade = Sensei()->question->get_question_grade( $question_id );

		$correct_answer = $show_correct_answers && ! $answer_correct ? self::get_correct_answer( $question_id ) : false;

		$grade = Sensei()->view_helper->format_question_points( $answer_grade . '/' . $question_grade );

		/**
		 * Filter the learner grade displayed.
		 *
		 * @since 3.14.0
		 *
		 * @hook  sensei_question_answer_message_grade
		 *
		 * @param {string} $grade          Formatted grade (eg "0/3 points")
		 * @param {int}    $lesson_id      Lesson ID.
		 * @param {int}    $question_id    Question ID.
		 * @param {int}    $user_id        User ID.
		 * @param {bool}   $answer_correct Whether this is the correct answer.
		 * @return {string} Answer message.
		 */
		$grade = apply_filters( 'sensei_question_answer_message_grade', $grade, $lesson_id, $question_id, get_current_user_id(), $answer_correct );

		/**
		 * Filter the correct answer.
		 *
		 * @since 1.9.0
		 *
		 * @hook  sensei_question_answer_message_correct_answer
		 *
		 * @param {string} $answer_message Answer message.
		 * @param {int}    $lesson_id      Lesson ID.
		 * @param {int}    $question_id    Question ID.
		 * @param {int}    $user_id        User ID.
		 * @param {bool}   $answer_correct Whether this is the correct answer.
		 * @return {string} Answer message.
		 */
		$correct_answer   = apply_filters( 'sensei_question_answer_message_correct_answer', $correct_answer, $lesson_id, $question_id, get_current_user_id(), $answer_correct );
		$has_answer_notes = $answer_notes && wp_strip_all_tags( $answer_notes );

		?>
		<?php if ( $indicate_incorrect || $has_answer_notes || $correct_answer ) { ?>
			<div class="sensei-lms-question__answer-feedback <?php echo esc_attr( implode( ' ', $answer_notes_classnames ) ); ?>">
				<?php if ( $indicate_incorrect ) { ?>
					<div class="sensei-lms-question__answer-feedback__header">
						<span class="sensei-lms-question__answer-feedback__icon"></span>
						<span
							class="sensei-lms-question__answer-feedback__title"><?php echo wp_kses_post( $answer_feedback_title ); ?></span>
						<?php if ( $grade && $question_grade > 0 ) { ?>
							<span class="sensei-lms-question__answer-feedback__points"><?php echo wp_kses_post( $grade ); ?></span>
						<?php } ?>
					</div>
				<?php } ?>
				<?php if ( $has_answer_notes || $correct_answer ) { ?>
					<div class="sensei-lms-question__answer-feedback__content">
						<?php if ( $correct_answer ) { ?>
							<div class="sensei-lms-question__answer-feedback__correct-answer">
								<?php echo wp_kses_post( __( 'Right Answer:', 'sensei-lms' ) ); ?>
								<?php echo wp_kses_post( $correct_answer ); ?>
							</div>
						<?php } ?>
						<?php if ( $has_answer_notes ) { ?>
							<div class="sensei-lms-question__answer-feedback__answer-notes">
								<?php echo wp_kses_post( $answer_notes ); ?>
							</div>
						<?php } ?>
					</div>
				<?php } ?>
			</div>
		<?php } ?>
		<?php if ( $grade ) { ?>
			<style> .question-title .grade { display: none; } </style>
		<?php } ?>
		<?php
	}

	/**
	 * Answer feedback.
	 *
	 * @deprecated 3.14.0 Renamed to the_answer_feedback
	 *
	 * @param int $question_id Question ID.
	 */
	public static function answer_feedback_notes( $question_id ) {
		_deprecated_function( __METHOD__, '3.14.0', 'Sensei_Question::the_answer_feedback' );
		self::the_answer_feedback( $question_id );
	}

	/**
	 * This function has to be run inside the quiz question loop on the single quiz page.
	 *
	 * It show the correct/incorrect answer per question depending on the quiz logic explained here:
	 * https://senseilms.com/documentation/quiz-settings-flowchart/
	 *
	 * Pseudo code for logic:  https://github.com/Automattic/sensei/issues/1422#issuecomment-214494263
	 *
	 * @since 1.9.0
	 * @deprecated 3.14.0 Moved into the_answer_feedback
	 */
	public static function the_answer_result_indication() {

		_deprecated_function( __METHOD__, '3.14.0', 'Sensei_Question::the_answer_feedback' );

		global $sensei_question_loop;

		$quiz_id            = $sensei_question_loop['quiz_id'];
		$question_item      = $sensei_question_loop['current_question'];
		$lesson_id          = Sensei()->quiz->get_lesson_id( $quiz_id );
		$user_lesson_status = Sensei_Utils::user_lesson_status( $lesson_id, get_current_user_id() );
		$quiz_graded        = isset( $user_lesson_status->comment_approved ) && ! in_array( $user_lesson_status->comment_approved, array( 'in-progress', 'ungraded' ) );

		if ( ! Sensei_Utils::has_started_course( Sensei()->lesson->get_course_id( $lesson_id ), get_current_user_id() ) ) {
			return;
		}

		if ( ! $quiz_graded ) {
			return;
		}

		$user_quiz_grade          = Sensei_Quiz::get_user_quiz_grade( $lesson_id, get_current_user_id() );
		$quiz_required_pass_grade = intval( get_post_meta( $quiz_id, '_quiz_passmark', true ) );
		$user_passed              = $user_quiz_grade >= $quiz_required_pass_grade;

		$show_answers = false;
		if ( ! Sensei_Quiz::is_pass_required( $lesson_id ) || $user_passed || ! Sensei_Quiz::is_reset_allowed( $lesson_id ) ) {
			$show_answers = true;
		}

		if ( ! $user_passed && ! Sensei_Quiz::get_option( $lesson_id, 'failed_indicate_incorrect', true ) ) {
			$show_answers = false;
		}

		/**
		 * Allow dynamic overriding of whether to show question answers or not
		 *
		 * @hook  sensei_question_show_answers
		 *
		 * @param {bool} $show_answers Whether to show the answer to the question.
		 * @param {int}  $question_id  Question ID.
		 * @param {int}  $quiz_id      Quiz ID.
		 * @param {int}  $lesson_id    Lesson ID.
		 * @param {int}  $user_id      User ID.
		 * @return {bool} Whether to show the answer to the question.
		 */
		$show_answers = apply_filters( 'sensei_question_show_answers', $show_answers, $question_item->ID, $quiz_id, $lesson_id, get_current_user_id() );

		if ( $show_answers ) {
			self::output_result_indication( $lesson_id, $question_item->ID );
			return;
		}
	}

	/**
	 * @since 1.9.5
	 * @deprecated 3.14.0 Moved into the_answer_feedback
	 *
	 * @param integer $lesson_id
	 * @param integer $question_id
	 */
	public static function output_result_indication( $lesson_id, $question_id ) {

		_deprecated_function( __METHOD__, '3.14.0', 'Sensei_Question::the_answer_feedback' );

		$question_grade      = Sensei()->question->get_question_grade( $question_id );
		$user_question_grade = Sensei()->quiz->get_user_question_grade( $lesson_id, $question_id, get_current_user_id() );

		// Defaults
		$answer_message = __( 'Incorrect - Right Answer:', 'sensei-lms' ) . ' ' . self::get_correct_answer( $question_id );

		if ( ! Sensei_Quiz::get_option( $lesson_id, 'failed_show_correct_answers', true ) ) {
			$answer_message = __( 'Incorrect', 'sensei-lms' );
		}

		// For zero grade mark as 'correct' but add no classes
		if ( 0 == $question_grade ) {
			$user_correct         = true;
			$answer_message_class = '';
			$answer_message       = '';
		} elseif ( $user_question_grade > 0 ) {
			$user_correct         = true;
			$answer_message_class = 'user_right';
			// translators: Placeholder is the question grade.
			$answer_message = sprintf( __( 'Grade: %d', 'sensei-lms' ), $user_question_grade );
		} else {
			$user_correct         = false;
			$answer_message_class = 'user_wrong';
		}

		// setup answer feedback class
		$answer_notes = Sensei()->quiz->get_user_question_feedback( $lesson_id, $question_id, get_current_user_id() );
		if ( $answer_notes ) {
			$answer_message_class .= ' has_notes';
		}

		/**
		 * Filter the answer message CSS classes.
		 *
		 * @hook  sensei_question_answer_message_css_class
		 *
		 * @param {string} $answer_notes_classname Space-separated CSS classes to apply to answer message.
		 * @param {int}    $lesson_id              Lesson ID.
		 * @param {int}    $question_id            Question ID.
		 * @param {int}    $user_id                User ID.
		 * @param {bool}   $answer_correct         Whether this is the correct answer.
		 * @return {string} Space-separated CSS classes to apply to answer message.
		 */
		$final_css_classes = apply_filters( 'sensei_question_answer_message_css_class', $answer_message_class, $lesson_id, $question_id, get_current_user_id(), $user_correct );

		/**
		 * Filter the answer message.
		 *
		 * @deprecated
		*
		 * @hook sensei_question_answer_message_text
		 *
		 * @param {string} $answer_message Answer message.
		 * @param {int}    $lesson_id      Lesson ID.
		 * @param {int}    $question_id    Question ID.
		 * @param {int}    $user_id        User ID.
		 * @param {bool}   $user_correct   Whether this is the correct answer.
		 * @return {string} Answer message.
		 */
		$final_message = apply_filters( 'sensei_question_answer_message_text', $answer_message, $lesson_id, $question_id, get_current_user_id(), $user_correct );
		?>
		<div class="answer_message <?php echo esc_attr( $final_css_classes ); ?>">

			<span><?php echo wp_kses_post( $final_message ); ?></span>

		</div>
		<?php
	}

	/**
	 * Generate the question template data and return it as an array.
	 *
	 * @since 1.9.0
	 *
	 * @param string  $question_id
	 * @param $quiz_id
	 * @return array $question_data
	 */
	public static function get_template_data( $question_id, $quiz_id ) {

		$lesson_id = (int) Sensei()->quiz->get_lesson_id( $quiz_id );
		$user_id   = (int) get_current_user_id();

		$reset_allowed = get_post_meta( $quiz_id, '_enable_quiz_reset', true );
		// Backwards compatibility.
		if ( 'on' === $reset_allowed ) {
			$reset_allowed = 1;
		}

		// Setup the question data.
		$data                           = [];
		$data['ID']                     = $question_id;
		$data['title']                  = get_the_title( $question_id );
		$data['content']                = get_post( $question_id )->post_content;
		$data['quiz_id']                = $quiz_id;
		$data['lesson_id']              = Sensei()->quiz->get_lesson_id( $quiz_id );
		$data['type']                   = Sensei()->question->get_question_type( $question_id );
		$data['question_grade']         = Sensei()->question->get_question_grade( $question_id );
		$data['user_question_grade']    = Sensei()->quiz->get_user_question_grade( $lesson_id, $question_id, $user_id );
		$data['question_right_answer']  = get_post_meta( $question_id, '_question_right_answer', true );
		$data['question_wrong_answers'] = get_post_meta( $question_id, '_question_wrong_answers', true );
		$data['user_answer_entry']      = Sensei()->quiz->get_user_question_answer( $lesson_id, $question_id, $user_id );
		$data['lesson_completed']       = Sensei_Utils::user_completed_lesson( $lesson_id, $user_id );
		$data['quiz_grade_type']        = get_post_meta( $quiz_id, '_quiz_grade_type', true );
		$data['reset_quiz_allowed']     = $reset_allowed;
		$data['quiz_is_completed']      = Sensei_Quiz::is_quiz_completed( $quiz_id, $user_id );
		$data['lesson_complete']        = $data['lesson_completed'];

		/**
		 * Filter the question template data. This filter fires in
		 * the get_template_data function.
		 *
		 * @since 1.9.0
		 *
		 * @hook sensei_get_question_template_data
		 *
		 * @param {array} $data        Question data.
		 * @param {int}   $question_id Question ID.
		 * @param {int}   $quiz_id     Quiz ID.
		 * @return {array} Question data.
		 */
		return apply_filters( 'sensei_get_question_template_data', $data, $question_id, $quiz_id );

	}

	/**
	 * Load multiple choice question data on the sensei_get_question_template_data filter.
	 *
	 * @since 1.9.0
	 *
	 * @param array  $question_data
	 * @param string $question_id
	 * @param string $quiz_id
	 *
	 * @return array()
	 */
	public static function file_upload_load_question_data( $question_data, $question_id, $quiz_id ) {

		if ( 'file-upload' === Sensei()->question->get_question_type( $question_id ) ) {

			// Get uploaded file.
			$attachment_id         = $question_data['user_answer_entry'];
			$answer_media_url      = '';
			$answer_media_filename = '';

			$question_helptext = '';
			if ( is_array( $question_data['question_wrong_answers'] ) && isset( $question_data['question_wrong_answers'][0] ) ) {

				$question_helptext = $question_data['question_wrong_answers'][0];

			}

			if ( 0 < intval( $attachment_id ) ) {

				$answer_media_url      = wp_get_attachment_url( $attachment_id );
				$filename_raw          = basename( $answer_media_url );
				$answer_media_filename = Sensei_Grading_User_Quiz::remove_hash_prefix( $filename_raw );

			}

			$upload_size = wp_max_upload_size();
			if ( ! $upload_size ) {
				$upload_size = 0;
			}

			// translators: Placeholder are the upload size and the measurement (e.g. 5 MB).
			$max_upload_size = sprintf( __( 'Maximum upload file size: %s', 'sensei-lms' ), esc_html( size_format( $upload_size ) ) );

			// Assemble all the data needed by the file upload template.
			$question_data['answer_media_url']      = $answer_media_url;
			$question_data['answer_media_filename'] = $answer_media_filename;
			$question_data['max_upload_size']       = $max_upload_size;

			$question_data['question_helptext'] = $question_helptext;

		}

		return $question_data;

	}

	/**
	 * Load multiple choice question data on the sensei_get_question_template_data
	 * filter.
	 *
	 * @since 1.9.0
	 *
	 * @param $question_data
	 * @param $question_id
	 * @param $quiz_id
	 *
	 * @return array()
	 */
	public static function multiple_choice_load_question_data( $question_data, $question_id, $quiz_id ) {

		if ( 'multiple-choice' == Sensei()->question->get_question_type( $question_id ) ) {

			$answer_type = 'radio';
			if ( is_array( $question_data['question_right_answer'] ) && ( 1 < count( $question_data['question_right_answer'] ) ) ) {

				$answer_type = 'checkbox';

			}

			// Merge right and wrong answers
			if ( ! is_array( $question_data['question_wrong_answers'] ) ) {
				$question_data['question_wrong_answers'] = [];
			}
			if ( is_array( $question_data['question_right_answer'] ) ) {

				$merged_options = array_merge( $question_data['question_wrong_answers'], $question_data['question_right_answer'] );

			} else {

				array_push( $question_data['question_wrong_answers'], $question_data['question_right_answer'] );
				$merged_options = $question_data['question_wrong_answers'];

			}

			// Setup answer options array.
			$question_answers_options = array();
			$count                    = 0;

			foreach ( $merged_options as $answer ) {

				$count++;
				$question_option = array();
				$is_quiz_graded  = isset( $question_data['user_quiz_grade'] );

				if ( ( $question_data['lesson_completed'] && $is_quiz_graded )
					|| ( $question_data['lesson_completed'] && ! $question_data['reset_quiz_allowed'] && $is_quiz_graded )
					|| ( 'auto' === $question_data['quiz_grade_type'] && ! $question_data['reset_quiz_allowed'] && $is_quiz_graded ) ) {

					$user_correct = false;

					// For zero grade mark as 'correct' but add no classes
					if ( 0 == $question_data['question_grade'] ) {
						$user_correct = true;

					} elseif ( $question_data['user_question_grade'] > 0 ) {

						$user_correct = true;

					}
				}

				// setup the option specific classes
				$answer_class = '';
				if ( isset( $user_correct ) && 0 < $question_data['question_grade'] ) {
					if ( is_array( $question_data['question_right_answer'] ) && in_array( $answer, $question_data['question_right_answer'] ) ) {

						$answer_class .= ' right_answer';

					} elseif ( ! is_array( $question_data['question_right_answer'] ) && $question_data['question_right_answer'] == $answer ) {

						$answer_class .= ' right_answer';

					} elseif ( ( is_array( $question_data['user_answer_entry'] ) && in_array( $answer, $question_data['user_answer_entry'] ) )
						|| ( ! $question_data['user_answer_entry'] && $question_data['user_answer_entry'] == $answer ) ) {

						$answer_class = 'user_wrong';
						if ( $user_correct ) {

							$answer_class = 'user_right';

						}
					}
				}

				// determine if the current option must be checked
				$checked = '';
				if ( isset( $question_data['user_answer_entry'] ) ) {
					if ( is_array( $question_data['user_answer_entry'] ) && in_array( $answer, $question_data['user_answer_entry'] ) ) {

						$checked = 'checked="checked"';

					} elseif ( ! is_array( $question_data['user_answer_entry'] ) ) {

						$checked = checked( $answer, $question_data['user_answer_entry'], false );

					}
				}

				// Load the answer option data
				$question_option['ID']           = Sensei()->lesson->get_answer_id( $answer );
				$question_option['answer']       = $answer;
				$question_option['option_class'] = $answer_class;
				$question_option['checked']      = $checked;
				$question_option['count']        = $count;
				$question_option['type']         = $answer_type;

				// add the speci  fic option to the list of options for this question
				$question_answers_options[ $question_option['ID'] ] = $question_option;

			}

			// Shuffle the array depending on the settings
			$answer_options_sorted = array();
			$random_order          = get_post_meta( $question_data['ID'], '_random_order', true );
			if ( $random_order && $random_order == 'yes' ) {

				$answer_options_sorted = $question_answers_options;
				shuffle( $answer_options_sorted );

			} else {

				$answer_order        = array();
				$answer_order_string = get_post_meta( $question_data['ID'], '_answer_order', true );
				if ( $answer_order_string ) {

					$answer_order = array_filter( explode( ',', $answer_order_string ) );
					if ( count( $answer_order ) > 0 ) {

						foreach ( $answer_order as $answer_id ) {

							if ( isset( $question_answers_options[ $answer_id ] ) ) {

								$answer_options_sorted[ $answer_id ] = $question_answers_options[ $answer_id ];
								unset( $question_answers_options[ $answer_id ] );

							}
						}

						if ( count( $question_answers_options ) > 0 ) {
							foreach ( $question_answers_options as $id => $answer ) {

								$answer_options_sorted[ $id ] = $answer;

							}
						}
					} else {

						$answer_options_sorted = $question_answers_options;

					}
				} else {

					$answer_options_sorted = $question_answers_options;

				}
			}

			// assemble and setup the data for the templates data array
			$question_data['answer_options'] = $answer_options_sorted;

		}

		return $question_data;

	}

	/**
	 * Load the gap fill question data on the sensei_get_question_template_data
	 * filter.
	 *
	 * @since 1.9.0
	 *
	 * @param $question_data
	 * @param $question_id
	 * @param $quiz_id
	 *
	 * @return array()
	 */
	public static function gap_fill_load_question_data( $question_data, $question_id, $quiz_id ) {

		if ( 'gap-fill' == Sensei()->question->get_question_type( $question_id ) ) {

			$gapfill_array                 = explode( '||', $question_data['question_right_answer'] );
			$question_data['gapfill_pre']  = isset( $gapfill_array[0] ) ? $gapfill_array[0] : '';
			$question_data['gapfill_gap']  = isset( $gapfill_array[1] ) ? $gapfill_array[1] : '';
			$question_data['gapfill_post'] = isset( $gapfill_array[2] ) ? $gapfill_array[2] : '';

		}

		return $question_data;

	}


	/**
	 * Get the correct answer for a question
	 *
	 * @param $question_id
	 * @return string $correct_answer or empty
	 */
	public static function get_correct_answer( $question_id ) {
		$right_answer = get_post_meta( $question_id, '_question_right_answer', true );
		$type         = Sensei()->question->get_question_type( $question_id );

		if ( 'boolean' == $type ) {
			if ( 'true' === $right_answer ) {
				$right_answer = __( 'True', 'sensei-lms' );
			} else {
				$right_answer = __( 'False', 'sensei-lms' );
			}
		} elseif ( 'multiple-choice' == $type ) {

			$right_answer = (array) $right_answer;
			$right_answer = esc_html( implode( ', ', $right_answer ) );

		} elseif ( 'gap-fill' == $type ) {

			$right_answer_array = explode( '||', $right_answer );
			if ( isset( $right_answer_array[0] ) ) {
				$gapfill_pre = esc_html( $right_answer_array[0] );
			} else {
				$gapfill_pre = ''; }
			if ( isset( $right_answer_array[1] ) ) {
				$gapfill_gap = esc_html( $right_answer_array[1] );
			} else {
				$gapfill_gap = ''; }
			if ( isset( $right_answer_array[2] ) ) {
				$gapfill_post = esc_html( $right_answer_array[2] );
			} else {
				$gapfill_post = ''; }

			$right_answer = $gapfill_pre . ' <span class="highlight">' . $gapfill_gap . '</span> ' . $gapfill_post;

		} else {

			// for non auto gradable question types no answer should be returned.
			$right_answer = '';

		}

		/**
		 * Filter the correct answer response.
		 *
		 * Can be used for text filters.
		 *
		 * @since 1.9.7
		 *
		 * @hook sensei_questions_get_correct_answer
		 *
		 * @param {string} $right_answer Correct answer.
		 * @param {int}    $question_id  Question ID.
		 * @return {string} Correct answer.
		 */
		return apply_filters( 'sensei_questions_get_correct_answer', $right_answer, $question_id );
	}

	/**
	 * Get answers by ID keys.
	 *
	 * @param string[] $answers Answers string.
	 *
	 * @return string[] Answers with the correct ID keys.
	 */
	public function get_answers_by_id( $answers = [] ) {
		$answers_by_id = [];

		foreach ( $answers as $answer ) {
			$answers_by_id[ Sensei()->lesson->get_answer_id( $answer ) ] = $answer;
		}

		return $answers_by_id;
	}

	/**
	 * Get answers sorted.
	 *
	 * @param string[]        $answers      Answers string by ID.
	 * @param string[]|string $answer_order Sorted answers IDs.
	 *
	 * @return string[] The sorted answers.
	 */
	public function get_answers_sorted( $answers, $answer_order ) {
		$answers_sorted = [];

		if ( is_string( $answer_order ) ) {
			$answer_order = explode( ',', $answer_order );
		}

		foreach ( $answer_order as $answer_id ) {
			if ( isset( $answers[ $answer_id ] ) ) {
				$answers_sorted[ $answer_id ] = $answers[ $answer_id ];
				unset( $answers[ $answer_id ] );
			}
		}

		if ( count( $answers ) > 0 ) {
			foreach ( $answers as $id => $answer ) {
				$answers_sorted[ $id ] = $answer;
			}
		}

		return $answers_sorted;
	}

	/**
	 * Log an event when a question is initially published.
	 *
	 * @since 2.1.0
	 * @access private
	 *
	 * @param WP_Post $question The question object.
	 */
	public function log_initial_publish_event( $question ) {
		$event_properties = [
			'page'          => 'unknown',
			'question_type' => $this->get_question_type( $question->ID ),
		];

		if ( function_exists( 'get_current_screen' ) ) {
			$screen = get_current_screen();

			if ( $screen && 'question' === $screen->id ) {
				$event_properties['page'] = 'question';
			} elseif ( isset( $_REQUEST['action'] ) && 'lesson_update_question' === $_REQUEST['action'] ) {
				$event_properties['page'] = 'lesson';
			}
		}

		sensei_log_event( 'question_add', $event_properties );
	}

	/**
	 * Check if a question can change to a new author. For normal questions, this is only possible if it
	 * doesn't belong to any other quiz that has a different author.
	 *
	 * @param int $question_id   The question post ID.
	 * @param int $new_author_id The new author ID.
	 *
	 * @return bool
	 */
	private function can_question_change_author( int $question_id, int $new_author_id ) {
		$question = get_post( $question_id );

		if ( ! $question || ! in_array( $question->post_type, [ 'question', 'multiple_question' ], true ) ) {
			return false;
		}

		if ( 'multiple_question' === $question->post_type ) {
			// These stick to the quiz. However, we don't attempt to change the questions in the category.
			return true;
		}

		$can_question_change_author = true;
		$quiz_ids                   = array_filter( get_post_meta( $question->ID, '_quiz_id' ) );
		foreach ( $quiz_ids as $quiz_id ) {
			$quiz = get_post( $quiz_id );
			if (
				$quiz
				&& 'quiz' === $quiz->post_type
				&& $new_author_id !== (int) $quiz->post_author
			) {
				$can_question_change_author = false;
				break;
			}
		}

		return $can_question_change_author;
	}

	/**
	 * Update the question author if possible.
	 *
	 * @param int $question_id   Question post ID.
	 * @param int $new_author_id New author.
	 *
	 * @return bool Whether the question author could be changed.
	 */
	public function maybe_update_question_author( int $question_id, int $new_author_id ) {
		if ( ! $question_id || ! $this->can_question_change_author( $question_id, $new_author_id ) ) {
			return false;
		}

		wp_update_post(
			[
				'ID'          => $question_id,
				'post_author' => $new_author_id,
			]
		);

		return true;
	}

}

/**
 * Class WooThemes_Sensei_Question
 *
 * @ignore only for backward compatibility
 * @since 1.9.0
 */
class WooThemes_Sensei_Question extends Sensei_Question{}