Source: includes/class-sensei-posttypes.php

<?php

use Sensei\Internal\Emails\Email_Post_Type;

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

/**
 * Sensei Post Types Class
 *
 * All functionality pertaining to the post types and taxonomies in Sensei.
 *
 * @package Core
 * @author Automattic
 *
 * @since 1.0.0
 */
class Sensei_PostTypes {
	const LEARNER_TAXONOMY_NAME = 'sensei_learner';

	/**
	 * Post types to exclude from sitemaps.
	 */
	const SITEMAPS_EXCLUDED_PUBLIC_POST_TYPES = [
		'quiz',
		'sensei_message',
		'lesson',
	];

	public $token;
	public $slider_labels;
	public $role_caps;

	/**
	 * @var Sensei_Course
	 */
	public $course;

	/**
	 * @var Sensei_Lesson
	 */
	public $lesson;

	/**
	 * @var Sensei_Question
	 */
	public $question;

	/**
	 * @var Sensei_Quiz
	 */
	public $quiz;

	/**
	 * Messages object.
	 *
	 * @var Sensei_Messages
	 */
	public $messages;

	/**
	 * Array of post ID's for which to fire an "initial publish" action.
	 *
	 * @var array
	 */
	private $initial_publish_post_ids = [];

	/**
	 * Constructor
	 *
	 * @since  1.0.0
	 */
	public function __construct() {

		// Setup Post Types
		$this->token = 'woothemes-sensei-posttypes';

		add_action( 'init', array( $this, 'setup_course_post_type' ), 10 );
		add_action( 'template_redirect', array( $this, 'redirect_course_archive_page' ) );
		add_action( 'init', array( $this, 'setup_lesson_post_type' ), 10 );
		add_action( 'init', array( $this, 'setup_quiz_post_type' ), 10 );
		add_action( 'init', array( $this, 'setup_question_post_type' ), 10 );
		add_action( 'init', array( $this, 'setup_multiple_question_post_type' ), 10 );
		add_action( 'init', array( $this, 'setup_sensei_message_post_type' ), 10 );

		// Setup Taxonomies
		add_action( 'init', array( $this, 'setup_learner_taxonomy' ), 10 );
		add_action( 'init', array( $this, 'setup_course_category_taxonomy' ), 10 );
		add_action( 'init', array( $this, 'setup_quiz_type_taxonomy' ), 10 );
		add_action( 'init', array( $this, 'setup_question_type_taxonomy' ), 10 );
		add_action( 'init', array( $this, 'setup_question_category_taxonomy' ), 10 );
		add_action( 'init', array( $this, 'setup_lesson_tag_taxonomy' ), 10 );

		// Load Post Type Objects
		$default_post_types = array(
			'course'   => 'Course',
			'lesson'   => 'Lesson',
			'quiz'     => 'Quiz',
			'question' => 'Question',
			'messages' => 'Messages',
		);
		$this->load_posttype_objects( $default_post_types );
		$this->set_role_cap_defaults( $default_post_types );

		// Admin functions
		if ( is_admin() ) {
			global $pagenow;
			if ( ( $pagenow == 'post.php' || $pagenow == 'post-new.php' ) ) {
				add_filter( 'enter_title_here', array( $this, 'enter_title_here' ), 10 );
				add_filter( 'post_updated_messages', array( $this, 'setup_post_type_messages' ) );
			}
		}

		// REST API functionality.
		add_action( 'rest_api_init', [ $this, 'setup_rest_api' ] );
		add_filter( 'rest_post_search_query', [ $this, 'exclude_post_types_from_rest_search' ] );

		// Add protections on feeds for certain CPTs.
		add_action( 'wp', [ $this, 'protect_feeds' ] );
		add_filter( 'wp_sitemaps_post_types', [ $this, 'exclude_sitemaps_post_types' ] );

		// Add 'Edit Quiz' link to admin bar
		add_action( 'admin_bar_menu', array( $this, 'quiz_admin_bar_menu' ), 81 );

		// Add Sensei LMS submenus.
		add_action( 'admin_menu', array( $this, 'add_submenus' ) );

		$this->setup_initial_publish_action();
	}

	/**
	 * Graceful fallback for deprecated properties.
	 *
	 * @since 4.24.4
	 *
	 * @param string $key The key to get.
	 *
	 * @return mixed
	 */
	public function __get( $key ) {
		if ( 'labels' === $key ) {
			_doing_it_wrong( __CLASS__ . '->labels', 'The "labels" property is deprecated.', '$$next-version$$' );

			return $this->get_main_post_type_labels();
		}
	}

	/**
	 * load_posttype_objects function.
	 * Dynamically loads post type objects for meta boxes on backend
	 *
	 * @access public
	 * @param array $posttypes (default: array())
	 * @return void
	 */
	public function load_posttype_objects( $posttypes = array() ) {

		foreach ( $posttypes as $posttype_token => $posttype_name ) {

			// Load the files
			$class_name                   = 'Sensei_' . $posttype_name;
			$this->$posttype_token        = new $class_name();
			$this->$posttype_token->token = $posttype_token;

		}
	}

	/**
	 * Set up REST API for post types.
	 *
	 * @access private
	 * @since 2.2.0
	 */
	public function setup_rest_api() {
		// Ensure registered meta will show up in the REST API for courses and lessons.
		add_post_type_support( 'course', 'custom-fields' );
		add_post_type_support( 'lesson', 'custom-fields' );

		// Hide post content for students who aren't enrolled.
		add_filter( 'post_password_required', [ $this, 'lesson_is_protected' ], 10, 2 );
	}

	/**
	 * Exclude post types from the REST API search.
	 *
	 * @since 4.24.4
	 * @access private
	 *
	 * @param array $args The query args.
	 * @return array The modified query args.
	 */
	public function exclude_post_types_from_rest_search( $args ) {
		$excluded_post_types = [
			'sensei_message',
			Email_Post_Type::POST_TYPE,
		];

		if ( isset( $args['post_type'] ) ) {
			$args['post_type'] = array_diff( (array) $args['post_type'], $excluded_post_types );
		}

		return $args;
	}

	/**
	 * Add protection to Sensei post type feeds.
	 *
	 * @access private
	 */
	public function protect_feeds() {
		if ( is_feed() && is_post_type_archive( [ 'lesson', 'question', 'quiz', 'sensei_message' ] ) ) {
			wp_die( esc_html__( 'Error: Feed does not exist', 'sensei-lms' ), '', [ 'response' => 404 ] );
		}
	}

	/**
	 * Exclude some post types from sitemaps.
	 *
	 * @param WP_Post_Type[] $post_types Array of post types.
	 *
	 * @return WP_Post_Type[]
	 */
	public function exclude_sitemaps_post_types( $post_types ) {
		return array_filter(
			$post_types,
			function( $post_type ) {
				return ! in_array( $post_type->name, self::SITEMAPS_EXCLUDED_PUBLIC_POST_TYPES, true );
			}
		);
	}

	/**
	 * Helper function to hide lesson post content by artificially making this a password protected post in certain contexts.
	 *
	 * @access private
	 *
	 * @param bool    $is_password_protected Filtered value for if this is a password protected post.
	 * @param WP_Post $post                  Post object.
	 *
	 * @return bool
	 */
	public function lesson_is_protected( $is_password_protected, $post ) {
		if (
			$post instanceof WP_Post
			&& 'lesson' === $post->post_type
			&& ! sensei_can_user_view_lesson( $post->ID, get_current_user_id() )
		) {
			return true;
		}

		return $is_password_protected;
	}

	/**
	 * Setup the "course" post type, it's admin menu item and the appropriate labels and permissions.
	 *
	 * @since  1.0.0
	 * @uses  Sensei()
	 * @return void
	 */
	public function setup_course_post_type() {
		// If Sensei LMS was first activated pre-3.7.0 and permalinks had a front value, `with_front` will be enabled.
		$with_front = Sensei()->get_legacy_flag( Sensei_Main::LEGACY_FLAG_WITH_FRONT ) ? true : false;

		$args = array(
			'labels'                => $this->get_all_post_type_labels( 'course' ),
			'public'                => true,
			'publicly_queryable'    => true,
			'show_ui'               => true,
			'show_in_menu'          => false,
			'show_in_admin_bar'     => true,
			'query_var'             => true,
			'rewrite'               => array(
				/**
				 * Filter the rewrite slug for the course post type.
				 *
				 * @hook sensei_course_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug'       => esc_attr( apply_filters( 'sensei_course_slug', _x( 'course', 'post type single url base', 'sensei-lms' ) ) ),
				'with_front' => $with_front,
				'feeds'      => true,
				'pages'      => true,
			),
			'map_meta_cap'          => true,
			'capability_type'       => 'course',
			'has_archive'           => $this->get_course_post_type_archive_slug(),
			'hierarchical'          => false,
			'menu_position'         => 51,
			'supports'              => array( 'title', 'editor', 'excerpt', 'thumbnail', 'revisions', 'custom-fields' ),
			'show_in_rest'          => true,
			'rest_base'             => 'courses',
			'rest_controller_class' => 'WP_REST_Posts_Controller',
		);

		/**
		 * Filter the arguments passed in when registering the Sensei Course post type.
		 *
		 * @since 1.9.0
		 *
		 * @hook sensei_register_post_type_course
		 *
		 * @param {array} $args The arguments passed in when registering the Sensei Course post type.
		 * @return {array} The filtered arguments.
		 */
		register_post_type( 'course', apply_filters( 'sensei_register_post_type_course', $args ) );
	}

	/**
	 * Redirect to the correct course archive link when using plain permalinks.
	 *
	 * @since 4.0.2
	 * @uses  Sensei()
	 * @access private
	 * @return void
	 */
	public function redirect_course_archive_page() {
		$settings = Sensei()->settings->settings;
		// phpcs:disable WordPress.Security.NonceVerification.Recommended
		// When default permalinks are enabled, redirect course page to post type archive url.
		$course_url = get_post_type_archive_link( 'course' );
		if (
			! empty( $_GET['page_id'] ) &&
			'' === get_option( 'permalink_structure' ) &&
			absint( $_GET['page_id'] ) === absint( $settings['course_page'] ) &&
			$course_url
		) {
			foreach ( $_GET as $param => $value ) {
				if ( 'page_id' !== $param ) {
					$course_url = add_query_arg( $param, $value, $course_url );
				}
			}
			wp_safe_redirect( esc_url_raw( $course_url ) );
			exit;
		}
	}

	/**
	 * Figure out of the course post type has an archive and what it should be.
	 *
	 * This function should return 'courses' or the page_uri for the course page setting.
	 *
	 * For backward compatibility  sake ( pre 1.9 )If the course page set in settings
	 * still has any of the old shortcodes: [newcourses][featuredcourses][freecourses][paidcourses] the
	 * page slug will not be returned. For any other pages without it the page URI will be returned.
	 *
	 * @sine 1.9.0
	 *
	 * @return false|string
	 */
	public function get_course_post_type_archive_slug() {

		$settings_course_page = get_post( Sensei()->settings->get( 'course_page' ) );

		// for a valid post that doesn't have any of the old short codes set the archive the same
		// as the page URI
		if ( is_a( $settings_course_page, 'WP_Post' ) && ! $this->has_old_shortcodes( $settings_course_page->post_content ) ) {

			return get_page_uri( $settings_course_page->ID );

		} else {

			return 'courses';

		}
	}

	/**
	 * Check if given content has any of these old shortcodes:
	 * [newcourses][featuredcourses][freecourses][paidcourses]
	 *
	 * @since 1.9.0
	 *
	 * @param string $content
	 *
	 * @return bool
	 */
	public function has_old_shortcodes( $content ) {

		return ( has_shortcode( $content, 'newcourses' )
		|| has_shortcode( $content, 'featuredcourses' )
		|| has_shortcode( $content, 'freecourses' )
		|| has_shortcode( $content, 'paidcourses' ) );
	}

	/**
	 * Setup the "lesson" post type, it's admin menu item and the appropriate labels and permissions.
	 *
	 * @since  1.0.0
	 * @uses  Sensei()
	 * @return void
	 */
	public function setup_lesson_post_type() {

		$supports_array = array( 'title', 'editor', 'excerpt', 'thumbnail', 'revisions' );
		$allow_comments = false;
		if ( isset( Sensei()->settings->settings['lesson_comments'] ) ) {
			$allow_comments = Sensei()->settings->settings['lesson_comments'];
		}
		if ( $allow_comments ) {
			array_push( $supports_array, 'comments' );
		}

		// If Sensei LMS was first activated pre-3.7.0 and permalinks had a front value, `with_front` will be enabled.
		$with_front = Sensei()->get_legacy_flag( Sensei_Main::LEGACY_FLAG_WITH_FRONT ) ? true : false;

		$args = array(
			'labels'                => $this->get_all_post_type_labels( 'lesson' ),
			'public'                => true,
			'publicly_queryable'    => true,
			'show_ui'               => true,
			'show_in_menu'          => false,
			'show_in_admin_bar'     => true,
			'query_var'             => true,
			'rewrite'               => array(
				/**
				 * Filter the rewrite slug for the lesson post type.
				 *
				 * @hook sensei_lesson_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug'       => esc_attr( apply_filters( 'sensei_lesson_slug', _x( 'lesson', 'post type single slug', 'sensei-lms' ) ) ),
				'with_front' => $with_front,
				'feeds'      => true,
				'pages'      => true,
			),
			'map_meta_cap'          => true,
			'capability_type'       => 'lesson',
			'has_archive'           => false,
			'hierarchical'          => false,
			'menu_position'         => 52,
			'supports'              => $supports_array,
			'show_in_rest'          => true,
			'rest_base'             => 'lessons',
			'rest_controller_class' => 'Sensei_REST_API_Lessons_Controller',
		);

		/**
		 * Filter the arguments passed in when registering the Sensei Lesson post type.
		 *
		 * @since 1.9.0
		 *
		 * @hook sensei_register_post_type_lesson
		 *
		 * @param {array} $args The arguments passed in when registering the Sensei Lesson post type.
		 * @return {array} The filtered arguments.
		 */
		register_post_type( 'lesson', apply_filters( 'sensei_register_post_type_lesson', $args ) );
	}

	/**
	 * Setup the "quiz" post type, it's admin menu item and the appropriate labels and permissions.
	 *
	 * @since  1.0.0
	 * @uses  Sensei()
	 * @return void
	 */
	public function setup_quiz_post_type() {
		// If Sensei LMS was first activated pre-3.7.0 and permalinks had a front value, `with_front` will be enabled.
		$with_front = Sensei()->get_legacy_flag( Sensei_Main::LEGACY_FLAG_WITH_FRONT ) ? true : false;

		$args = array(
			'labels'              => $this->get_all_post_type_labels( 'quiz' ),
			'public'              => true,
			'publicly_queryable'  => true,
			'show_ui'             => true,
			'show_in_menu'        => false,
			'show_in_admin_bar'   => true,
			'show_in_nav_menus'   => false,
			'query_var'           => true,
			'exclude_from_search' => true,
			'rewrite'             => array(
				/**
				 * Filter the rewrite slug for the quiz post type.
				 *
				 * @hook sensei_quiz_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug'       => esc_attr( apply_filters( 'sensei_quiz_slug', _x( 'quiz', 'post type single slug', 'sensei-lms' ) ) ),
				'with_front' => $with_front,
				'feeds'      => true,
				'pages'      => true,
			),
			'map_meta_cap'        => true,
			'capability_type'     => 'quiz',
			'capabilities'        => array(
				'edit_published_posts' => 'do_not_allow',
			),
			'has_archive'         => false,
			'hierarchical'        => false,
			'menu_position'       => 20, // Below "Pages"
			'supports'            => array( '' ),
		);

		/**
		 * Filter the arguments passed in when registering the Sensei Quiz post type.
		 *
		 * @since 1.9.0
		 *
		 * @hook sensei_register_post_type_quiz
		 *
		 * @param {array} $args The arguments passed in when registering the Sensei Quiz post type.
		 * @return {array} The filtered arguments.
		 */
		register_post_type( 'quiz', apply_filters( 'sensei_register_post_type_quiz', $args ) );
	}

	/**
	 * Setup the "question" post type, it's admin menu item and the appropriate labels and permissions.
	 *
	 * @since  1.0.0
	 * @return void
	 */
	public function setup_question_post_type() {
		// If Sensei LMS was first activated pre-3.7.0 and permalinks had a front value, `with_front` will be enabled.
		$with_front = Sensei()->get_legacy_flag( Sensei_Main::LEGACY_FLAG_WITH_FRONT ) ? true : false;

		$args = array(
			'labels'                => $this->get_all_post_type_labels( 'question' ),
			'public'                => false,
			'publicly_queryable'    => false,
			'show_ui'               => true,
			'show_in_menu'          => false,
			'show_in_admin_bar'     => true,
			'show_in_nav_menus'     => false,
			'query_var'             => true,
			'exclude_from_search'   => true,
			'rewrite'               => false,
			'map_meta_cap'          => true,
			'capability_type'       => 'question',
			'has_archive'           => false,
			'hierarchical'          => false,
			'menu_position'         => 51,
			'supports'              => array( 'title', 'editor', 'revisions' ),
			'show_in_rest'          => true,
			'rest_base'             => 'questions',
			'rest_controller_class' => 'Sensei_REST_API_Questions_Controller',
		);

		/**
		 * Filter the arguments passed in when registering the Sensei Question post type.
		 *
		 * @since 1.9.0
		 *
		 * @hook sensei_register_post_type_question
		 *
		 * @param {array} $args The arguments passed in when registering the Sensei Question post type.
		 * @return {array} The filtered arguments.
		 */
		register_post_type( 'question', apply_filters( 'sensei_register_post_type_question', $args ) );
	}

	/**
	 * Setup the "multiple_question" post type, it's admin menu item and the appropriate labels and permissions.
	 *
	 * @since  1.6.0
	 * @return void
	 */
	public function setup_multiple_question_post_type() {

		$args = array(
			'labels'              => $this->get_all_post_type_labels( 'multiple_question' ),
			'public'              => false,
			'publicly_queryable'  => false,
			'show_ui'             => false,
			'show_in_menu'        => false,
			'show_in_nav_menus'   => false,
			'query_var'           => false,
			'exclude_from_search' => true,
			'rewrite'             => array(
				/**
				 * Filter the rewrite slug for the multiple_question post type.
				 *
				 * @hook sensei_multiple_question_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug'       => esc_attr( apply_filters( 'sensei_multiple_question_slug', _x( 'multiple_question', 'post type single slug', 'sensei-lms' ) ) ),
				'with_front' => false,
				'feeds'      => false,
				'pages'      => false,
			),
			'map_meta_cap'        => true,
			'capability_type'     => 'question',
			'has_archive'         => false,
			'hierarchical'        => false,
			'menu_position'       => 51,
			'supports'            => array( 'title', 'custom-fields' ),
		);

		register_post_type( 'multiple_question', $args );
	}

	/**
	 * Setup the "sensei_message" post type, it's admin menu item and the appropriate labels and permissions.
	 *
	 * @since  1.6.0
	 * @return void
	 */
	public function setup_sensei_message_post_type() {

		if ( ! isset( Sensei()->settings->settings['messages_disable'] ) || ! Sensei()->settings->settings['messages_disable'] ) {

			$args = array(
				'labels'                => $this->get_all_post_type_labels( 'sensei_message' ),
				'public'                => true,
				'publicly_queryable'    => true,
				'show_ui'               => true,
				'show_in_menu'          => false,
				'show_in_nav_menus'     => true,
				'query_var'             => true,
				'exclude_from_search'   => true,
				'rewrite'               => array(
					/**
					 * Filter the rewrite slug for the Sensei Message post type.
					 *
					 * @hook sensei_messages_slug
					 *
					 * @param {string} $slug The rewrite slug.
					 * @return {string} The filtered rewrite slug.
					 */
					'slug'       => esc_attr( apply_filters( 'sensei_messages_slug', _x( 'messages', 'post type single slug', 'sensei-lms' ) ) ),
					'with_front' => false,
					'feeds'      => false,
					'pages'      => true,
				),
				'map_meta_cap'          => true,
				'capability_type'       => 'question',
				'has_archive'           => true,
				'hierarchical'          => false,
				'menu_position'         => 50,
				'show_in_rest'          => true,
				'rest_base'             => 'sensei-messages',
				'rest_controller_class' => 'Sensei_REST_API_Messages_Controller',
				'supports'              => array( 'title', 'editor', 'comments' ),
				'delete_with_user'      => true,
			);

			/**
			 * Filter the arguments passed in when registering the Sensei sensei_message post type.
			 *
			 * @since 1.9.0
			 *
			 * @hook sensei_register_post_type_sensei_message
			 *
			 * @param {array} $args The arguments passed in when registering the Sensei sensei_message post type.
			 * @return {array} The filtered arguments.
			 */
			register_post_type( 'sensei_message', apply_filters( 'sensei_register_post_type_sensei_message', $args ) );
		}
	}

	/**
	 * Registers the learner taxonomy.
	 *
	 * @access private
	 */
	public function setup_learner_taxonomy() {
		register_taxonomy(
			self::LEARNER_TAXONOMY_NAME,
			'course',
			[
				'public'  => false,
				'show_ui' => false,
			]
		);
	}

	/**
	 * Setup the "course category" taxonomy, linked to the "course" post type.
	 *
	 * @since  1.1.0
	 * @return void
	 */
	public function setup_course_category_taxonomy() {
		// "Course Categories" Custom Taxonomy
		$labels = array(
			'name'              => _x( 'Course Categories', 'taxonomy general name', 'sensei-lms' ),
			'singular_name'     => _x( 'Course Category', 'taxonomy singular name', 'sensei-lms' ),
			'search_items'      => __( 'Search Course Categories', 'sensei-lms' ),
			'all_items'         => __( 'All Course Categories', 'sensei-lms' ),
			'parent_item'       => __( 'Parent Course Category', 'sensei-lms' ),
			'parent_item_colon' => __( 'Parent Course Category:', 'sensei-lms' ),
			'view_item'         => __( 'View Course Category', 'sensei-lms' ),
			'edit_item'         => __( 'Edit Course Category', 'sensei-lms' ),
			'update_item'       => __( 'Update Course Category', 'sensei-lms' ),
			'add_new_item'      => __( 'Add New Course Category', 'sensei-lms' ),
			'new_item_name'     => __( 'New Course Category Name', 'sensei-lms' ),
			'menu_name'         => __( 'Course Categories', 'sensei-lms' ),
			'popular_items'     => null, // Hides the "Popular" section above the "add" form in the admin.
			'back_to_items'     => __( '&larr; Back to Course Categories', 'sensei-lms' ),
		);

		$args = array(
			'hierarchical'      => true,
			'labels'            => $labels,
			'show_in_rest'      => true,
			'show_ui'           => true,
			'show_in_menu'      => false,
			'query_var'         => true,
			'show_in_nav_menus' => true,
			'capabilities'      => array(
				'manage_terms' => 'manage_course_categories',
				'edit_terms'   => 'manage_course_categories',
				'delete_terms' => 'manage_course_categories',
				'assign_terms' => 'edit_courses',
			),
			'rewrite'           => array(
				/**
				 * Filter the rewrite slug for the course category taxonomy.
				 *
				 * @hook sensei_course_category_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug' => esc_attr( apply_filters( 'sensei_course_category_slug', _x( 'course-category', 'taxonomy archive slug', 'sensei-lms' ) ) ),
			),
		);

		register_taxonomy( 'course-category', array( 'course' ), $args );
	}

	/**
	 * Setup the "quiz type" taxonomy, linked to the "quiz" post type.
	 *
	 * @since  1.0.0
	 * @return void
	 */
	public function setup_quiz_type_taxonomy() {

		// "Quiz Types" Custom Taxonomy
		$labels = array(
			'name'              => _x( 'Quiz Types', 'taxonomy general name', 'sensei-lms' ),
			'singular_name'     => _x( 'Quiz Type', 'taxonomy singular name', 'sensei-lms' ),
			'search_items'      => __( 'Search Quiz Types', 'sensei-lms' ),
			'all_items'         => __( 'All Quiz Types', 'sensei-lms' ),
			'parent_item'       => __( 'Parent Quiz Type', 'sensei-lms' ),
			'parent_item_colon' => __( 'Parent Quiz Type:', 'sensei-lms' ),
			'edit_item'         => __( 'Edit Quiz Type', 'sensei-lms' ),
			'update_item'       => __( 'Update Quiz Type', 'sensei-lms' ),
			'add_new_item'      => __( 'Add New Quiz Type', 'sensei-lms' ),
			'new_item_name'     => __( 'New Quiz Type Name', 'sensei-lms' ),
			'menu_name'         => __( 'Quiz Types', 'sensei-lms' ),
			'popular_items'     => null, // Hides the "Popular" section above the "add" form in the admin.
		);

		$args = array(
			'hierarchical'      => false,
			'labels'            => $labels,
			'show_ui'           => true, /* TO DO - future releases */
			'query_var'         => true,
			'show_in_nav_menus' => false,
			'public'            => false,
			'rewrite'           => array(
				/**
				 * Filter the rewrite slug for the quiz type taxonomy.
				 *
				 * @hook sensei_quiz_type_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug' => esc_attr( apply_filters( 'sensei_quiz_type_slug', _x( 'quiz-type', 'taxonomy archive slug', 'sensei-lms' ) ) ),
			),
		);

		register_taxonomy( 'quiz-type', array( 'quiz' ), $args );
	}

	/**
	 * Setup the "question type" taxonomy, linked to the "question" post type.
	 *
	 * @since  1.3.0
	 * @return void
	 */
	public function setup_question_type_taxonomy() {

		// "Question Types" Custom Taxonomy
		$labels = array(
			'name'              => _x( 'Question Types', 'taxonomy general name', 'sensei-lms' ),
			'singular_name'     => _x( 'Question Type', 'taxonomy singular name', 'sensei-lms' ),
			'search_items'      => __( 'Search Question Types', 'sensei-lms' ),
			'all_items'         => __( 'All Question Types', 'sensei-lms' ),
			'parent_item'       => __( 'Parent Question Type', 'sensei-lms' ),
			'parent_item_colon' => __( 'Parent Question Type:', 'sensei-lms' ),
			'edit_item'         => __( 'Edit Question Type', 'sensei-lms' ),
			'update_item'       => __( 'Update Question Type', 'sensei-lms' ),
			'add_new_item'      => __( 'Add New Question Type', 'sensei-lms' ),
			'new_item_name'     => __( 'New Question Type Name', 'sensei-lms' ),
			'menu_name'         => __( 'Question Types', 'sensei-lms' ),
			'popular_items'     => null, // Hides the "Popular" section above the "add" form in the admin.
		);

		$args = array(
			'hierarchical'      => false,
			'labels'            => $labels,
			'show_ui'           => false,
			'public'            => false,
			'query_var'         => false,
			'show_in_nav_menus' => false,
			'show_admin_column' => true,
			'show_in_rest'      => true,
			'rewrite'           => array(
				/**
				 * Filter the rewrite slug for the question type taxonomy.
				 *
				 * @hook sensei_question_type_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug' => esc_attr( apply_filters( 'sensei_question_type_slug', _x( 'question-type', 'taxonomy archive slug', 'sensei-lms' ) ) ),
			),
		);

		register_taxonomy( 'question-type', array( 'question' ), $args );
	}

	/**
	 * Setup the "question category" taxonomy, linked to the "question" post type.
	 *
	 * @since  1.3.0
	 * @return void
	 */
	public function setup_question_category_taxonomy() {
		// "Question Categories" Custom Taxonomy
		$labels = array(
			'name'              => _x( 'Question Categories', 'taxonomy general name', 'sensei-lms' ),
			'singular_name'     => _x( 'Question Category', 'taxonomy singular name', 'sensei-lms' ),
			'search_items'      => __( 'Search Question Categories', 'sensei-lms' ),
			'all_items'         => __( 'All Question Categories', 'sensei-lms' ),
			'parent_item'       => __( 'Parent Question Category', 'sensei-lms' ),
			'parent_item_colon' => __( 'Parent Question Category:', 'sensei-lms' ),
			'view_item'         => __( 'View Question Category', 'sensei-lms' ),
			'edit_item'         => __( 'Edit Question Category', 'sensei-lms' ),
			'update_item'       => __( 'Update Question Category', 'sensei-lms' ),
			'add_new_item'      => __( 'Add New Question Category', 'sensei-lms' ),
			'new_item_name'     => __( 'New Question Category Name', 'sensei-lms' ),
			'menu_name'         => __( 'Categories', 'sensei-lms' ),
			'back_to_items'     => __( '&larr; Back to Question Categories', 'sensei-lms' ),
		);

		$args = array(
			'hierarchical'      => true,
			'labels'            => $labels,
			'show_ui'           => true,
			'public'            => false,
			'query_var'         => false,
			'show_in_nav_menus' => false,
			'show_admin_column' => true,
			'show_in_rest'      => true,
			'capabilities'      => array(
				'manage_terms' => 'manage_question_categories',
				'edit_terms'   => 'manage_question_categories',
				'delete_terms' => 'manage_question_categories',
				'assign_terms' => 'edit_questions',
			),
			'rewrite'           => array(
				/**
				 * Filter the rewrite slug for the question category taxonomy.
				 *
				 * @hook sensei_question_category_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug' => esc_attr( apply_filters( 'sensei_question_category_slug', _x( 'question-category', 'taxonomy archive slug', 'sensei-lms' ) ) ),
			),
		);

		register_taxonomy( 'question-category', array( 'question' ), $args );
	}

	/**
	 * Setup the "lesson tags" taxonomy, linked to the "lesson" post type.
	 *
	 * @since  1.5.0
	 * @return void
	 */
	public function setup_lesson_tag_taxonomy() {
		// "Lesson Tags" Custom Taxonomy
		$labels = array(
			'name'              => _x( 'Lesson Tags', 'taxonomy general name', 'sensei-lms' ),
			'singular_name'     => _x( 'Lesson Tag', 'taxonomy singular name', 'sensei-lms' ),
			'search_items'      => __( 'Search Lesson Tags', 'sensei-lms' ),
			'all_items'         => __( 'All Lesson Tags', 'sensei-lms' ),
			'parent_item'       => __( 'Parent Tag', 'sensei-lms' ),
			'parent_item_colon' => __( 'Parent Tag:', 'sensei-lms' ),
			'view_item'         => __( 'View Lesson Tag', 'sensei-lms' ),
			'edit_item'         => __( 'Edit Lesson Tag', 'sensei-lms' ),
			'update_item'       => __( 'Update Lesson Tag', 'sensei-lms' ),
			'add_new_item'      => __( 'Add New Lesson Tag', 'sensei-lms' ),
			'new_item_name'     => __( 'New Tag Name', 'sensei-lms' ),
			'menu_name'         => __( 'Lesson Tags', 'sensei-lms' ),
			'back_to_items'     => __( '&larr; Back to Lesson Tags', 'sensei-lms' ),
		);

		$args = array(
			'hierarchical'      => false,
			'labels'            => $labels,
			'show_in_rest'      => true,
			'show_ui'           => true,
			'query_var'         => true,
			'show_in_nav_menus' => true,
			'capabilities'      => array(
				'manage_terms' => 'manage_lesson_categories',
				'edit_terms'   => 'manage_lesson_categories',
				'delete_terms' => 'manage_lesson_categories',
				'assign_terms' => 'edit_lessons',
			),
			'rewrite'           => array(
				/**
				 * Filter the rewrite slug for the lesson tag taxonomy.
				 *
				 * @hook sensei_lesson_tag_slug
				 *
				 * @param {string} $slug The rewrite slug.
				 * @return {string} The filtered rewrite slug.
				 */
				'slug' => esc_attr( apply_filters( 'sensei_lesson_tag_slug', _x( 'lesson-tag', 'taxonomy archive slug', 'sensei-lms' ) ) ),
			),
		);

		register_taxonomy( 'lesson-tag', array( 'lesson' ), $args );
	}

	/**
	 * Get the singular, plural and menu label names for the post types.
	 *
	 * @param string|null $post_type The post type.
	 *
	 * @return array
	 */
	private function get_main_post_type_labels( $post_type = null ) {
		$labels = array(
			'course'            => array(
				'singular' => __( 'Course', 'sensei-lms' ),
				'plural'   => __( 'Courses', 'sensei-lms' ),
				'menu'     => __( 'Courses', 'sensei-lms' ),
			),
			'lesson'            => array(
				'singular' => __( 'Lesson', 'sensei-lms' ),
				'plural'   => __( 'Lessons', 'sensei-lms' ),
				'menu'     => __( 'Lessons', 'sensei-lms' ),
			),
			'quiz'              => array(
				'singular' => __( 'Quiz', 'sensei-lms' ),
				'plural'   => __( 'Quizzes', 'sensei-lms' ),
				'menu'     => __( 'Quizzes', 'sensei-lms' ),
			),
			'question'          => array(
				'singular' => __( 'Question', 'sensei-lms' ),
				'plural'   => __( 'Questions', 'sensei-lms' ),
				'menu'     => __( 'Questions', 'sensei-lms' ),
			),
			'multiple_question' => array(
				'singular' => __( 'Multiple Question', 'sensei-lms' ),
				'plural'   => __( 'Multiple Questions', 'sensei-lms' ),
				'menu'     => __( 'Multiple Questions', 'sensei-lms' ),
			),
			'sensei_message'    => array(
				'singular' => __( 'Message', 'sensei-lms' ),
				'plural'   => __( 'Messages', 'sensei-lms' ),
				'menu'     => __( 'Messages', 'sensei-lms' ),
			),
		);

		return $post_type ? $labels[ $post_type ] : $labels;
	}

	/**
	 * Create the labels for a specified post type.
	 *
	 * @param  string $post_type The post type.
	 * @return array             An array of the labels to be used
	 */
	private function get_all_post_type_labels( $post_type ) {
		$labels   = $this->get_main_post_type_labels( $post_type );
		$singular = $labels['singular'];
		$plural   = $labels['plural'];
		$menu     = $labels['menu'];

		$lower_case_plural = function_exists( 'mb_strtolower' ) ? mb_strtolower( $plural, 'UTF-8' ) : strtolower( $plural );

		return array(
			'name'               => $plural,
			'singular_name'      => $singular,
			'add_new'            => __( 'Add New', 'sensei-lms' ),
			// translators: Placeholder is the singular post type label.
			'add_new_item'       => sprintf( __( 'Add New %s', 'sensei-lms' ), $singular ),
			// translators: Placeholder is the item title/name.
			'edit_item'          => sprintf( __( 'Edit %s', 'sensei-lms' ), $singular ),
			// translators: Placeholder is the singular post type label.
			'new_item'           => sprintf( __( 'New %s', 'sensei-lms' ), $singular ),
			// translators: Placeholder is the plural post type label.
			'all_items'          => $plural,
			// translators: Placeholder is the singular post type label.
			'view_item'          => sprintf( __( 'View %s', 'sensei-lms' ), $singular ),
			// translators: Placeholder is the plural post type label.
			'search_items'       => sprintf( __( 'Search %s', 'sensei-lms' ), $plural ),
			// translators: Placeholder is the lower-case plural post type label.
			'not_found'          => sprintf( __( 'No %s found', 'sensei-lms' ), $lower_case_plural ),
			// translators: Placeholder is the lower-case plural post type label.
			'not_found_in_trash' => sprintf( __( 'No %s found in Trash', 'sensei-lms' ), $lower_case_plural ),
			'parent_item_colon'  => '',
			'menu_name'          => $menu,
		);
	}

	/**
	 * Setup update messages for the post types.
	 *
	 * @since  1.0.0
	 * @param  array $messages The existing array of messages for post types.
	 * @return array           The modified array of messages for post types.
	 */
	public function setup_post_type_messages( $messages ) {
		$messages['course']            = $this->create_post_type_messages( 'course' );
		$messages['lesson']            = $this->create_post_type_messages( 'lesson' );
		$messages['quiz']              = $this->create_post_type_messages( 'quiz' );
		$messages['question']          = $this->create_post_type_messages( 'question' );
		$messages['multiple_question'] = $this->create_post_type_messages( 'multiple_question' );

		return $messages;
	}

	/**
	 * Create an array of messages for a specified post type.
	 *
	 * @since  1.0.0
	 * @param  string $post_type The post type for which to create messages.
	 * @return array            An array of messages (empty array if the post type isn't one we're looking to work with).
	 */
	private function create_post_type_messages( $post_type ) {
		global $post, $post_ID;

		$labels = $this->get_main_post_type_labels( $post_type );

		$messages = array(
			0  => '',
			// translators: Placeholders are the singular label for the post type and the post's permalink, respectively.
			1  => sprintf( __( '%1$s updated. %2$sView %1$s%3$s.', 'sensei-lms' ), $labels['singular'], '<a href="' . esc_url( get_permalink( $post_ID ) ) . '">', '</a>' ),
			2  => __( 'Custom field updated.', 'sensei-lms' ),
			3  => __( 'Custom field deleted.', 'sensei-lms' ),
			// translators: Placeholder is the singular label for the post type.
			4  => sprintf( __( '%1$s updated.', 'sensei-lms' ), $labels['singular'] ),
			// translators: Placeholders are the singular label for the post type and the post's revision, respectively.
			5  => isset( $_GET['revision'] ) ? sprintf( __( '%1$s restored to revision from %2$s.', 'sensei-lms' ), $labels['singular'], wp_post_revision_title( (int) $_GET['revision'], false ) ) : false,
			// translators: Placeholders are the singular label for the post type and the post's permalink, respectively.
			6  => sprintf( __( '%1$s published. %2$sView %1$s%3$s.', 'sensei-lms' ), $labels['singular'], '<a href="' . esc_url( get_permalink( $post_ID ) ) . '">', '</a>' ),
			// translators: Placeholder is the singular label for the post type.
			7  => sprintf( __( '%1$s saved.', 'sensei-lms' ), $labels['singular'] ),
			// translators: Placeholders are the singular label for the post type and the post's preview link, respectively.
			8  => sprintf( __( '%1$s submitted. %2$sPreview %1$s%3$s.', 'sensei-lms' ), $labels['singular'], '<a target="_blank" href="' . esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) . '">', '</a>' ),
			/*
			 * translators: Placeholders are as follows (in order):
			 *
			 * - The singular label for the post type.
			 * - The formatted post date.
			 * - The opening tag for the post's permalink.
			 * - The closing tag for the post's permalink.
			 */
			9  => sprintf( __( '%1$s scheduled for: %2$s. %3$sPreview %4$s%5$s.', 'sensei-lms' ), $labels['singular'], '<strong>' . date_i18n( __( 'M j, Y @ G:i', 'sensei-lms' ), strtotime( $post->post_date ) ) . '</strong>', '<a target="_blank" href="' . esc_url( get_permalink( $post_ID ) ) . '">', $labels['singular'], '</a>' ),
			// translators: Placeholders are the singular label for the post type and the post's preview link, respectively.
			10 => sprintf( __( '%1$s draft updated. %2$sPreview %3$s%4$s.', 'sensei-lms' ), $labels['singular'], '<a target="_blank" href="' . esc_url( add_query_arg( 'preview', 'true', get_permalink( $post_ID ) ) ) . '">', $labels['singular'], '</a>' ),
		);

		return $messages;
	}

	/**
	 * Change the "Enter Title Here" text for the "slide" post type.
	 *
	 * @access public
	 * @since  1.0.0
	 * @param  string $title
	 * @return string $title
	 */
	public function enter_title_here( $title ) {
		if ( get_post_type() == 'course' ) {
			$title = __( 'Course name', 'sensei-lms' );
		} elseif ( get_post_type() == 'lesson' ) {
			$title = __( 'Lesson name', 'sensei-lms' );
		}

		return $title;
	}

	/**
	 * Assigns the defaults for each user role capabilities.
	 *
	 * @since  1.1.0
	 *
	 * @param array $post_types
	 * @return void
	 */
	public function set_role_cap_defaults( $post_types = array() ) {

		foreach ( $post_types as $post_type_item => $post_type_name ) {
			// Super Admin
			$this->role_caps[] = array(
				'administrator' => array(
					'edit_' . $post_type_item,
					'read_' . $post_type_item,
					'delete_' . $post_type_item,
					'create_' . $post_type_item . 's',
					'edit_' . $post_type_item . 's',
					'edit_others_' . $post_type_item . 's',
					'publish_' . $post_type_item . 's',
					'read_private_' . $post_type_item . 's',
					'read',
					'delete_' . $post_type_item . 's',
					'delete_private_' . $post_type_item . 's',
					'delete_published_' . $post_type_item . 's',
					'delete_others_' . $post_type_item . 's',
					'edit_private_' . $post_type_item . 's',
					'edit_published_' . $post_type_item . 's',
					'manage_sensei',
					'manage_sensei_grades',
				),
				'editor'        => array(
					'edit_' . $post_type_item,
					'read_' . $post_type_item,
					'delete_' . $post_type_item,
					'create_' . $post_type_item . 's',
					'edit_' . $post_type_item . 's',
					'edit_others_' . $post_type_item . 's',
					'publish_' . $post_type_item . 's',
					'read_private_' . $post_type_item . 's',
					'read',
					'delete_' . $post_type_item . 's',
					'delete_private_' . $post_type_item . 's',
					'delete_published_' . $post_type_item . 's',
					'delete_others_' . $post_type_item . 's',
					'edit_private_' . $post_type_item . 's',
					'edit_published_' . $post_type_item . 's',
				),
				'author'        => array(
					'edit_' . $post_type_item,
					'read_' . $post_type_item,
					'delete_' . $post_type_item,
					'create_' . $post_type_item . 's',
					'edit_' . $post_type_item . 's',
					'publish_' . $post_type_item . 's',
					'read',
					'delete_' . $post_type_item . 's',
					'delete_published_' . $post_type_item . 's',
					'edit_published_' . $post_type_item . 's',
				),
				'contributor'   => array(
					'edit_' . $post_type_item,
					'read_' . $post_type_item,
					'delete_' . $post_type_item,
					'create_' . $post_type_item . 's',
					'edit_' . $post_type_item . 's',
					'read',
					'delete_' . $post_type_item . 's',
				),
				'subscriber'    => array( 'read' ),

			);
		}
	}

	/**
	 * Adds a 'Edit Quiz' link to the admin bar when viewing a Quiz linked to a corresponding Lesson
	 *
	 * @since  1.7.0
	 * @param WP_Admin_Bar $bar
	 * @return void
	 */
	public function quiz_admin_bar_menu( $bar ) {
		if ( is_single() && 'quiz' == get_queried_object()->post_type ) {
			$lesson_id = get_post_meta( get_queried_object()->ID, '_quiz_lesson', true );
			if ( $lesson_id ) {
				$object_type = get_post_type_object( 'quiz' );
				$bar->add_menu(
					array(
						'id'    => 'edit',
						'title' => $object_type->labels->edit_item,
						'href'  => get_edit_post_link( $lesson_id ),
					)
				);
			}
		}
	}

	/**
	 * Add submenus under "Sensei LMS" main menu.
	 *
	 * @since 4.0.0
	 */
	public function add_submenus() {
		Sensei_Home::instance()->add_admin_menu_item();

		add_submenu_page(
			'sensei',
			__( 'Courses', 'sensei-lms' ),
			__( 'Courses', 'sensei-lms' ),
			'edit_courses',
			'edit.php?post_type=course'
		);

		add_submenu_page(
			'sensei',
			__( 'Modules', 'sensei-lms' ),
			__( 'Modules', 'sensei-lms' ),
			'manage_categories',
			'edit-tags.php?taxonomy=module&post_type=course'
		);

		add_submenu_page(
			'sensei',
			__( 'Lessons', 'sensei-lms' ),
			__( 'Lessons', 'sensei-lms' ),
			'edit_lessons',
			'edit.php?post_type=lesson'
		);

		add_submenu_page(
			'sensei',
			__( 'Questions', 'sensei-lms' ),
			__( 'Questions', 'sensei-lms' ),
			'edit_questions',
			'edit.php?post_type=question'
		);

		Sensei()->learners->learners_admin_menu();

		/**
		 * Fires when the Sensei Pro Groups menu item should be added.
		 *
		 * @since 4.5.0
		 *
		 * @hook sensei_pro_groups_menu_item
		 */
		do_action( 'sensei_pro_groups_menu_item', [] );

		/**
		 * Filters the Student groups promo landing page.
		 *
		 * @since 4.5.2
		 *
		 * @hook sensei_student_groups_hide
		 *
		 * @param {bool} $sensei_student_groups_hide Whether to hide the Student Groups promo landing page.
		 * @return {bool} Whether to hide the Student groups landing page.
		 */
		if ( ! apply_filters( 'sensei_student_groups_hide', false ) ) {
			$instance = new Sensei_Groups_Landing_Page();
			$instance->add_groups_landing_page_menu_item();
		}

		Sensei()->grading->grading_admin_menu();

		$sensei_messages = new Sensei_Messages();
		$sensei_messages->add_menu_item();

		Sensei()->analysis->analysis_admin_menu();
		Sensei()->settings->register_settings_screen();
		Sensei_Tools::instance()->add_menu_pages();
	}

	/**
	 * Setup firing of the "initial publish" action for Sensei CPT's. This will
	 * set up hooks to track when posts are published, and to fire the "initial
	 * publish" action at the correct time.
	 *
	 * However, this action will not be fired for posts that are created through
	 * the REST API. This is because of an edge case with the block editor. When
	 * a post is published through the block editor, the "initial publish"
	 * action will be fired when the metabox save request is posted, rather than
	 * when the initial API request is posted.
	 *
	 * Note that the REST API restriction can be removed when we migrate all
	 * meta information for the block editor away from metaboxes and into
	 * blocks.
	 *
	 * @since 2.1.0
	 * @access private
	 */
	public function setup_initial_publish_action() {
		$this->reset_scheduled_initial_publish_actions();

		// Schedule an action for initial publish of Sensei CPT's.
		add_action( 'transition_post_status', [ $this, 'maybe_schedule_initial_publish_action' ], 10, 3 );

		// Fire all scheduled actions on shutdown.
		add_action( 'shutdown', [ $this, 'fire_scheduled_initial_publish_actions' ] );

		// Never fire actions on REST API request.
		add_action( 'rest_api_init', [ $this, 'disable_fire_scheduled_initial_publish_actions' ] );
	}

	/**
	 * Disable the scheduled "initial publish" actions from being fired. This is
	 * called on `rest_api_init`.
	 *
	 * @since 2.1.0
	 * @access private
	 */
	public function disable_fire_scheduled_initial_publish_actions() {
		remove_action( 'shutdown', [ $this, 'fire_scheduled_initial_publish_actions' ] );
	}

	/**
	 * This hook is run on `post_status_transition` to schedule the "initial
	 * publish" action if needed.
	 *
	 * Posts will be marked as already published if the old status is `publish`,
	 * so that we do not fire the "initial publish" action for existing publish
	 * posts when they are re-published.
	 *
	 * For newly published posts, we schedule the "initial publish" action to be
	 * fired at the end of the request.
	 *
	 * Note that we do not mark as published if this is a metabox update
	 * request. In this case, the REST API request has already handled this, so
	 * we just need to schedule the action if needed.
	 *
	 * @since 2.1.0
	 * @access private
	 *
	 * @param string  $new_status The new post status.
	 * @param string  $old_status The old post status.
	 * @param WP_Post $post       The post.
	 */
	public function maybe_schedule_initial_publish_action( $new_status, $old_status, $post ) {
		// Only handle Sensei post types.
		if ( ! $this->is_sensei_post_type_for_initial_publish_action( $post->post_type ) ) {
			return;
		}

		// If the old status is `publish`, mark as already published.
		if ( 'publish' === $old_status && ! $this->is_meta_box_save_request() ) {
			$this->mark_post_already_published( $post->ID );
		}

		// If transitioning to `publish` for the first time, schedule the action.
		if ( 'publish' === $new_status && ! $this->check_post_already_published( $post->ID ) ) {
			$this->schedule_initial_publish_action( $post->ID );
		}
	}

	/**
	 * Fire the scheduled "initial publish" actions. This is run on `shutdown`.
	 *
	 * @since 2.1.0
	 *
	 * @internal
	 */
	public function fire_scheduled_initial_publish_actions() {
		foreach ( array_unique( $this->initial_publish_post_ids ) as $post_id ) {
			$post = get_post( $post_id );
			if ( $post ) {
				/**
				 * Fires the scheduled "initial publish" actions for a post on `shutdown`.
				 *
				 * @since 2.1.0
				 *
				 * @hook sensei_{$post_type}_initial_publish
				 *
				 * @param {WP_Post} $post The post.
				 */
				do_action( "sensei_{$post->post_type}_initial_publish", $post );
				$this->mark_post_already_published( $post->ID );
			}
		}

		// Clear the finished post ID's.
		$this->reset_scheduled_initial_publish_actions();
	}

	/**
	 * Determine whether the current request is a "meta box save" request
	 * (typically run by the block editor).
	 *
	 * @since 2.1.0
	 * @access private
	 */
	private function is_meta_box_save_request() {
		// phpcs:ignore WordPress.Security.NonceVerification
		return isset( $_REQUEST['meta-box-loader'] ) && '1' === $_REQUEST['meta-box-loader'];
	}

	/**
	 * Schedule an "initial publish" action for the given post ID.
	 *
	 * @since 2.1.0
	 * @access private
	 *
	 * @param int $post_id The post ID.
	 */
	private function schedule_initial_publish_action( $post_id ) {
		$this->initial_publish_post_ids[] = $post_id;
	}

	/**
	 * Reset the array of post ID's for which to fire "initial publish" actions.
	 *
	 * @since 2.1.0
	 * @access private
	 */
	private function reset_scheduled_initial_publish_actions() {
		$this->initial_publish_post_ids = [];
	}

	/**
	 * Check if post type is one for which we should fire the "initial publish"
	 * action.
	 *
	 * @since 2.1.0
	 *
	 * @param string $post_type The post type.
	 * @return bool
	 */
	private function is_sensei_post_type_for_initial_publish_action( $post_type ) {
		/**
		 * Filter the post types for which to fire an action on initial publish.
		 *
		 * @since 2.1.0
		 *
		 * @param array $post_types The post types.
		 */
		$post_types = apply_filters(
			'sensei_post_types_for_initial_publish_action',
			[
				'course',
				'lesson',
				'quiz',
				'question',
				'sensei_message',
			]
		);
		return in_array( $post_type, $post_types, true );
	}

	/**
	 * Mark the given post as "already published".
	 *
	 * @since 2.1.0
	 *
	 * @param string $post_id The post ID.
	 */
	private function mark_post_already_published( $post_id ) {
		add_post_meta( $post_id, '_sensei_already_published', true, true );
	}

	/**
	 * Check whether the post is marked as "already published".
	 *
	 * @since 2.1.0
	 *
	 * @param string $post_id The post ID.
	 * @return bool
	 */
	private function check_post_already_published( $post_id ) {
		return get_post_meta( $post_id, '_sensei_already_published', true );
	}
}

/**
 * Class WooThemes_Sensei_PostTypes
 *
 * @ignore only for backward compatibility
 * @since 1.9.0
 */
class WooThemes_Sensei_PostTypes extends Sensei_PostTypes{}