actions = array(
'preview_curriculum_item' => array(
'callback' => array( $this, 'preview_curriculum_item' ),
'capability' => 'manage_academy_instructor',
),
'get_course_slug' => array(
'callback' => array( $this, 'get_course_slug' ),
'capability' => 'manage_academy_instructor'
),
'fetch_course_category' => array(
'callback' => array( $this, 'fetch_course_category' ),
'capability' => 'manage_academy_instructor'
),
'render_enrolled_courses' => array(
'callback' => array( $this, 'render_enrolled_courses' ),
'capability' => 'read'
),
'render_pending_enrolled_courses' => array(
'callback' => array( $this, 'render_enrolled_courses' ),
'capability' => 'read'
),
'render_wishlist_courses' => array(
'callback' => array( $this, 'render_wishlist_courses' ),
'capability' => 'read'
),
'course_add_to_wishlist' => array(
'callback' => array( $this, 'course_add_to_wishlist' ),
'allow_visitor_action' => true
),
'course_add_to_favorite' => array(
'callback' => array( $this, 'course_add_to_favorite' ),
'capability' => 'read'
),
'get_my_courses' => array(
'callback' => array( $this, 'get_my_courses' ),
'capability' => 'manage_academy_instructor'
),
'enroll_course' => array(
'callback' => array( $this, 'enroll_course' ),
'allow_visitor_action' => true
),
'complete_course' => array(
'callback' => array( $this, 'complete_course' ),
'capability' => 'read'
),
'add_course_review' => array(
'callback' => array( $this, 'add_course_review' ),
'capability' => 'read'
),
'get_course_details' => array(
'callback' => array( $this, 'get_course_details' ),
'capability' => 'read'
),
'import_course' => array(
'callback' => array( $this, 'import_all_courses' ),
'capability' => 'manage_academy_instructor',
),
'validate_youtube_api' => array(
'callback' => array( $this, 'save_youtube_api_key' ),
'capability' => 'manage_options',
),
'import_from_playlist' => [
'callback' => [ $this, 'import_from_playlist' ],
'capability' => 'manage_options',
],
'get_sticky_courses_priorities' => array(
'callback' => array( $this, 'get_sticky_courses_priorities' ),
'capability' => 'manage_academy_instructor',
),
'reorder_sticky_courses' => array(
'callback' => array( $this, 'reorder_sticky_courses' ),
'capability' => 'manage_academy_instructor',
),
);
}
/**
* Lists the other featured courses and their priority (`menu_order`) so
* the course editor can show, and drag-reorder, where this course ranks
* among them. `exclude_id` leaves out the course being edited — the client
* splices that one in itself at its in-progress priority.
*
* Ordered the same way the catalog orders featured courses, so the list
* mirrors what a visitor actually sees.
*
* @param array $payload_data
*/
public function get_sticky_courses_priorities( $payload_data ) {
$payload = Sanitizer::sanitize_payload( array(
'exclude_id' => 'integer',
), $payload_data );
$exclude_id = ! empty( $payload['exclude_id'] ) ? $payload['exclude_id'] : 0;
$sticky_courses = get_posts( array(
'post_type' => 'academy_courses',
'post_status' => array( 'publish', 'draft', 'pending', 'private', 'future' ),
'posts_per_page' => -1, // phpcs:ignore WordPressVIPMinimum.Performance.NoPaging.posts_per_page_posts_per_page -- admin reorder screen needs every featured course; a hand-picked, short list.
'meta_query' => array( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
array(
'key' => \Academy\Helper::STICKY_META_KEY,
'value' => '1',
),
),
'orderby' => array(
'menu_order' => 'ASC',
'date' => 'DESC',
),
) );
$result = array();
foreach ( $sticky_courses as $course ) {
// Excluded here rather than with post__not_in, which defeats the query cache.
if ( (int) $exclude_id === (int) $course->ID ) {
continue;
}
// Every instructor can see the featured ORDER — it is public
// information the moment the catalog renders — but the titles of
// courses that are not yet public are not. Show those as a
// placeholder rather than leaking another author's draft.
$is_visible = 'publish' === $course->post_status || current_user_can( 'edit_post', $course->ID );
$result[] = array(
'id' => $course->ID,
'title' => $is_visible ? get_the_title( $course ) : esc_html__( 'Another featured course', 'academy' ),
'menu_order' => (int) $course->menu_order,
'can_edit' => current_user_can( 'edit_post', $course->ID ),
);
}
wp_send_json_success( $result );
}
/**
* Persists a new drag-order for featured courses from the course editor's
* priority list. `order` is an array of course IDs in their new
* top-to-bottom sequence; a course's 1-based position becomes its
* `menu_order`. Position 0 is left unused so it keeps meaning "no priority
* set yet" (see `Database::maybe_assign_sticky_priority()`).
*
* Every course is authorised individually with `edit_post`: holding
* `manage_academy_instructor` is enough to reach this endpoint, but not to
* reorder a course you cannot edit — otherwise any instructor could demote
* a colleague's featured course and promote their own. IDs that fail the
* check, are not `academy_courses` posts, or are no longer featured are
* skipped, so a stale client cannot resurrect them either.
*
* The course open in the editor is included here too — its own
* `menu_order` Formik field is kept in sync client-side, but this endpoint
* is what persists it and every other listed course immediately, since the
* rest of the list is not part of this course's save payload.
*
* @param array $payload_data
*/
public function reorder_sticky_courses( $payload_data ) {
$payload = Sanitizer::sanitize_payload( array(
'order' => 'array',
), $payload_data );
$order = ! empty( $payload['order'] ) && is_array( $payload['order'] ) ? array_values( $payload['order'] ) : array();
if ( empty( $order ) ) {
wp_send_json_error( esc_html__( 'No course order provided.', 'academy' ) );
}
$updated = 0;
$skipped = 0;
foreach ( $order as $index => $course_id ) {
$course_id = absint( $course_id );
if ( ! $course_id || 'academy_courses' !== get_post_type( $course_id ) ) {
continue;
}
if ( ! \Academy\Helper::is_course_sticky( $course_id ) ) {
continue;
}
if ( ! current_user_can( 'edit_post', $course_id ) ) {
++$skipped;
continue;
}
$priority = $index + 1;
if ( (int) get_post_field( 'menu_order', $course_id ) === $priority ) {
continue;
}
$result = wp_update_post(
array(
'ID' => $course_id,
'menu_order' => $priority,
),
true
);
if ( ! is_wp_error( $result ) ) {
++$updated;
}
}//end foreach
wp_send_json_success( array(
'updated' => $updated,
'skipped' => $skipped,
) );
}
public function get_course_slug( $payload_data ) {
$payload = Sanitizer::sanitize_payload([
'ID' => 'integer',
'new_title' => 'string',
'new_slug' => 'string',
], $payload_data );
// get_sample_permalink() rebuilds the slug from the title for any
// non-null name, so an empty one must be null to show the saved slug.
$new_slug = ! empty( $payload['new_slug'] ) ? $payload['new_slug'] : null;
wp_send_json_success( Helper::get_sample_permalink_args( $payload['ID'], $payload['new_title'], $new_slug ) );
}
/**
* Redirect to a curriculum item's frontend learn page. Because the learn URL
* depends on the `is_enabled_lessons_php_render` setting (pretty permalink vs
* React-player hash route) and on the item's slug — neither of which the
* course builder holds — the URL is resolved here via the shared helper and
* the browser is redirected. Opened in a new tab by the builder's preview
* links (course row eye icons / editors).
*/
public function preview_curriculum_item() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- nonce already verified in handle_ajax_request().
$course_id = isset( $_GET['course_id'] ) ? absint( $_GET['course_id'] ) : 0;
$type = isset( $_GET['type'] ) ? sanitize_key( wp_unslash( $_GET['type'] ) ) : '';
$item_id = isset( $_GET['item_id'] ) ? absint( $_GET['item_id'] ) : 0;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$allowed_types = array( 'lesson', 'quiz', 'assignment', 'meeting', 'booking', 'quizpress_quiz' );
if ( ! $course_id || ! $item_id || ! in_array( $type, $allowed_types, true ) ) {
wp_die( esc_html__( 'Invalid preview request.', 'academy' ) );
}
$url = Helper::get_topic_play_link( array(
'type' => $type,
'id' => $item_id,
), $course_id );
if ( empty( $url ) ) {
$url = get_the_permalink( $course_id );
}
wp_safe_redirect( $url );
exit;
}
public function fetch_course_category( $payload_data ) {
$payload = Sanitizer::sanitize_payload([
'postId' => 'integer',
'keyword' => 'string',
'type' => 'string',
], $payload_data );
$catId = ( isset( $payload['postId'] ) ? $payload['postId'] : 0 );
$keyword = ( isset( $payload['keyword'] ) ? $payload['keyword'] : '' );
$type = ( isset( $payload['type'] ) ? $payload['type'] : 'single' );
$categories = [];
if ( ! empty( $keyword ) ) {
$categories = get_term_by( 'name', $keyword, 'academy_courses_category' );
} elseif ( $catId && 'single' === $type ) {
$categories = get_term( $catId, 'academy_courses_category' );
} else {
$categories = get_terms( array(
'taxonomy' => 'academy_courses_category',
'hide_empty' => false,
) );
}
$results = [];
if ( is_array( $categories ) && count( $categories ) ) {
foreach ( $categories as $category ) {
$results[] = array(
'label' => $category->name,
'value' => $category->term_id,
);
}
}
wp_send_json_success( $results );
}
public function render_enrolled_courses( $payload_data ) {
$payload = Sanitizer::sanitize_payload([
'request_type' => 'string',
], $payload_data );
$request_type = ( isset( $payload['request_type'] ) ? $payload['request_type'] : 'enrolled' );
$user_id = get_current_user_id();
$enrolled_course_ids = \Academy\Helper::get_enrolled_courses_ids_by_user( $user_id );
$complete_course_ids = \Academy\Helper::get_complete_courses_ids_by_user( $user_id );
$post_in = $enrolled_course_ids;
if ( 'complete' === $request_type ) {
$post_in = $complete_course_ids;
} elseif ( 'active' === $request_type ) {
$post_in = array_diff( $enrolled_course_ids, $complete_course_ids );
}
// An empty post__in is ignored by WP_Query (it would list every course), so pin it to nothing.
$post_in = $post_in ? array_values( $post_in ) : array( 0 );
$course_args = array(
'post_type' => 'academy_courses',
'post_status' => 'publish',
'post__in' => $post_in,
'posts_per_page' => count( $post_in ),
);
$courses = new \WP_Query( apply_filters( 'academy/enrolled_courses_args', $course_args ) );
ob_start();
?>
have_posts() ) :
while ( $courses->have_posts() ) :
$courses->the_post();
$ID = get_the_ID();
$rating = \Academy\Helper::get_course_rating( $ID );
$total_topics = \Academy\Helper::get_total_number_of_course_topics( $ID );
$total_completed_topics = \Academy\Helper::get_total_number_of_completed_course_topics_by_course_and_student_id( $ID );
$percentage = \Academy\Helper::calculate_percentage( $total_topics, $total_completed_topics );
?>
rating_avg );
?>
rating_avg ); ?> rating_count . ')' ); ?>
$output
) );
}
public function render_pending_enrolled_courses() {
$user_id = get_current_user_id();
$pending_enrolled_course_ids = \Academy\Helper::get_pending_enrolled_courses_ids_by_user( $user_id );
if ( ! count( $pending_enrolled_course_ids ) ) {
wp_send_json_success( [] );
}
$course_args = array(
'post_type' => 'academy_courses',
'post_status' => 'publish',
'post__in' => $pending_enrolled_course_ids,
'posts_per_page' => count( $pending_enrolled_course_ids ),
);
$courses = new \WP_Query( apply_filters( 'academy/pending_enrolled_course_args', $course_args ) );
$response = [];
if ( count( $pending_enrolled_course_ids ) && $courses->have_posts() ) {
while ( $courses->have_posts() ) :
$courses->the_post();
$response[] = array(
'ID' => get_the_ID(),
'permalink' => get_the_permalink(),
'title' => get_the_title(),
);
endwhile;
wp_reset_query();
}
wp_send_json_success( $response );
}
public function render_wishlist_courses() {
$courses = \Academy\Helper::get_wishlist_courses_by_user( get_current_user_id(), array( 'private', 'publish' ) );
ob_start();
?>
have_posts() ) :
while ( $courses->have_posts() ) :
$courses->the_post();
\Academy\Helper::get_template_part( 'content', 'course' );
endwhile;
wp_reset_query();
else :
?>
$markup ) );
}
wp_send_json_error( array( 'redirect_to' => wp_login_url( wp_get_referer() ) ) );
}
global $wpdb;
$payload = Sanitizer::sanitize_payload([
'course_id' => 'integer',
], $payload_data );
$course_id = $payload['course_id'];
$user_id = get_current_user_id();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$is_already_in_list = $wpdb->get_row( $wpdb->prepare( "SELECT * from {$wpdb->usermeta} WHERE user_id = %d AND meta_key = 'academy_course_wishlist' AND meta_value = %d;", $user_id, $course_id ) );
if ( $is_already_in_list ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete(
$wpdb->usermeta,
array(
'user_id' => $user_id,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_key' => 'academy_course_wishlist',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'meta_value' => $course_id,
)
);
wp_send_json_success( array( 'is_added' => false ) );
}
add_user_meta( $user_id, 'academy_course_wishlist', $course_id );
wp_send_json_success( array( 'is_added' => true ) );
}
public function course_add_to_favorite( $payload_data ) {
global $wpdb;
$payload = Sanitizer::sanitize_payload([
'course_id' => 'integer',
], $payload_data );
$course_id = $payload['course_id'];
$user_id = get_current_user_id();
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$is_already_in_list = $wpdb->get_row( $wpdb->prepare( "SELECT * from {$wpdb->usermeta} WHERE user_id = %d AND meta_key = 'academy_course_favorite' AND meta_value = %d;", $user_id, $course_id ) );
if ( $is_already_in_list ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete(
$wpdb->usermeta,
array(
'user_id' => $user_id,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
'meta_key' => 'academy_course_favorite',
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
'meta_value' => $course_id,
)
);
wp_send_json_success( array( 'is_added' => false ) );
}
add_user_meta( $user_id, 'academy_course_favorite', $course_id );
wp_send_json_success( array( 'is_added' => true ) );
}
public function get_my_courses() {
$response = [];
// phpcs:disable WordPressVIPMinimum.Performance.NoPaging.posts_per_page_posts_per_page -- needs the complete (small, bounded) set
$course_args = array(
'post_type' => 'academy_courses',
'post_status' => 'publish',
'author' => get_current_user_id(),
'posts_per_page' => -1,
);
// phpcs:enable WordPressVIPMinimum.Performance.NoPaging.posts_per_page_posts_per_page
$courses = new \WP_Query( apply_filters( 'academy/my_courses_args', $course_args ) );
if ( $courses->have_posts() ) :
while ( $courses->have_posts() ) :
$courses->the_post();
$ID = get_the_ID();
$rating = \Academy\Helper::get_course_rating( $ID );
$rating_markup = \Academy\Helper::star_rating_generator( $rating->rating_avg );
$total_enrolled = \Academy\Helper::count_course_enrolled( $ID );
$response[] = array(
'title' => get_the_title( $ID ),
'permalink' => get_the_permalink( $ID ),
'rating' => $rating,
'rating_markup' => $rating_markup,
'total_enrolled' => $total_enrolled
);
endwhile;
wp_reset_query();
endif;
wp_send_json_success( $response );
}
public function enroll_course( $payload_data ) {
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'is_required_logged_in' => true ) );
}
$user_id = get_current_user_id();
$payload = Sanitizer::sanitize_payload([
'course_id' => 'integer',
], $payload_data );
$course_id = (int) $payload['course_id'];
// Only a published course takes enrollments from this endpoint. Addons
// that enroll through it (e.g. free course bundles) add their post type.
$enrollable_post_types = apply_filters( 'academy/enroll_course_post_types', array( 'academy_courses' ) );
if ( ! in_array( get_post_type( $course_id ), $enrollable_post_types, true ) || 'publish' !== get_post_status( $course_id ) ) {
wp_send_json_error( __( 'This course is not available for enrollment.', 'academy' ) );
}
$course_type = \Academy\Helper::get_course_type( $course_id );
$is_enrolled = false;
$course_type = apply_filters( 'academy/before_enroll_course_type', $course_type, $course_id );
if ( 'free' === $course_type || 'public' === $course_type ) {
$is_enrolled = \Academy\Helper::do_enroll( $course_id, $user_id );
}
if ( $is_enrolled ) {
wp_send_json_success( __( 'Successfully Enrolled.', 'academy' ) );
}
wp_send_json_error( __( 'Failed to enrolled course.', 'academy' ) );
}
public function complete_course( $payload_data ) {
$user_id = get_current_user_id();
$payload = Sanitizer::sanitize_payload([
'course_id' => 'integer',
], $payload_data );
$course_id = (int) $payload['course_id'];
// Only a learner of a published course can complete it (and earn its
// certificate): enrolled, or any logged-in user on a public course.
if ( 'academy_courses' !== get_post_type( $course_id ) || 'publish' !== get_post_status( $course_id ) ) {
wp_send_json_error( __( 'This course is not available.', 'academy' ) );
}
if ( ! \Academy\Helper::is_enrolled( $course_id, $user_id ) && ! \Academy\Helper::is_public_course( $course_id ) ) {
wp_send_json_error( __( 'You are not enrolled in this course.', 'academy' ) );
}
$has_incomplete_topic = false;
$curriculum_lists = \Academy\Helper::get_course_curriculum( $course_id );
foreach ( $curriculum_lists as $curriculum_list ) {
if ( is_array( $curriculum_list['topics'] ) ) {
foreach ( $curriculum_list['topics'] as $topic ) {
if ( empty( $topic['is_completed'] ) && 'sub-curriculum' !== $topic['type'] ) {
$has_incomplete_topic = true;
break;
}
if ( isset( $topic['topics'] ) && is_array( $topic['topics'] ) ) {
foreach ( $topic['topics'] as $child_topic ) {
if ( empty( $child_topic['is_completed'] ) ) {
$has_incomplete_topic = true;
break;
}
}
}
}
}
// found incomplete topic then break loop
if ( $has_incomplete_topic ) {
break;
}
}//end foreach
if ( $has_incomplete_topic ) {
wp_send_json_error( __( 'To complete this course, please make sure that you have finished all the topics.', 'academy' ) );
}
do_action( 'academy/admin/course_complete_before', $course_id );
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$completed = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(comment_ID) from {$wpdb->comments}
WHERE comment_agent = 'academy' AND comment_type = 'course_completed'
AND comment_post_ID = %d AND user_id = %d",
$course_id, $user_id
),
);
if ( $completed > 0 ) {
wp_send_json_error( __( 'You have already completed this course.', 'academy' ) );
}
$date = gmdate( 'Y-m-d H:i:s', \Academy\Helper::get_time() );
// hash is unique.
do {
$hash = substr( md5( wp_generate_password( 32 ) . $date . $course_id . $user_id ), 0, 16 );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$hasHash = (int) $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT(comment_ID) from {$wpdb->comments}
WHERE comment_agent = 'academy' AND comment_type = 'course_completed' AND comment_content = %s ",
$hash
)
);
} while ( $hasHash > 0 );
$data = array(
'comment_post_ID' => $course_id,
'comment_author' => $user_id,
'comment_date' => $date,
'comment_date_gmt' => get_gmt_from_date( $date ),
'comment_content' => $hash,
'comment_approved' => 'approved',
'comment_agent' => 'academy',
'comment_type' => 'course_completed',
'user_id' => $user_id,
);
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$is_complete = $wpdb->insert( $wpdb->comments, $data );
do_action( 'academy/admin/course_complete_after', $course_id, $user_id );
if ( $is_complete ) {
wp_send_json_success( __( 'Successfully Completed.', 'academy' ) );
}
wp_send_json_error( __( 'Failed, try again.', 'academy' ) );
}
public function add_course_review( $payload_data ) {
$payload = Sanitizer::sanitize_payload([
'course_id' => 'integer',
'rating' => 'integer',
'review' => 'post',
], $payload_data );
$course_id = $payload['course_id'];
$user_id = get_current_user_id();
$current_user = get_userdata( $user_id );
$rating = (int) $payload['rating'];
$review = $payload['review'];
// Same rules as the REST review route: a real course, reviews enabled,
// a valid rating, and an enrolled user who completed it (or an admin).
if ( ! $current_user || ! $course_id || 'academy_courses' !== get_post_type( $course_id ) ) {
wp_send_json_error( __( 'Course not found.', 'academy' ), 404 );
}
if ( $rating < 1 || $rating > 5 ) {
wp_send_json_error( __( 'Rating must be between 1 and 5.', 'academy' ), 400 );
}
if ( ! (bool) \Academy\Helper::get_settings( 'is_enabled_course_review', true ) || (bool) get_post_meta( $course_id, 'academy_is_disabled_course_review', true ) ) {
wp_send_json_error( __( 'Course reviews are disabled.', 'academy' ), 403 );
}
if ( ! current_user_can( 'manage_options' ) && ! ( \Academy\Helper::is_enrolled( $course_id, $user_id ) && \Academy\Helper::is_completed_course( $course_id, $user_id ) ) ) {
wp_send_json_error( __( 'You must enroll in and complete this course before submitting a review.', 'academy' ), 403 );
}
$data = array(
'comment_post_ID' => $course_id,
'comment_content' => $review,
'user_id' => $current_user->ID,
'comment_author' => $current_user->user_login,
'comment_author_email' => $current_user->user_email,
'comment_author_url' => $current_user->user_url,
'comment_type' => 'academy_courses',
'comment_approved' => '1',
'comment_meta' => array(
'academy_rating' => $rating,
)
);
// get all review of current user
$existing_reviews = get_comments(array(
'comment_type' => 'academy_courses',
'post_id' => $course_id,
'user_id' => $current_user->ID,
));
// if the review exist then update it
if ( count( $existing_reviews ) ) {
$existing_review = current( $existing_reviews );
$data['comment_ID'] = $existing_review->comment_ID;
$is_update = wp_update_comment( $data );
if ( $is_update ) {
wp_send_json_success(array(
'message' => __( 'Successfully Updated Review.', 'academy' ),
'redirect_url' => get_the_permalink( $course_id ),
));
}
}
// insert the review
$comment_id = wp_insert_comment( $data );
if ( $comment_id ) {
wp_send_json_success(array(
'message' => __( 'Successfully Added Review.', 'academy' ),
'redirect_url' => get_the_permalink( $course_id ),
));
}
wp_send_json_error( __( 'Sorry, Failed to add review.', 'academy' ) );
}
public function get_course_details( $payload_data ) {
$student_id = get_current_user_id();
$payload = Sanitizer::sanitize_payload([
'courseID' => 'integer',
], $payload_data );
$course_id = isset( $payload['courseID'] ) ? $payload['courseID'] : 0;
$is_administrator = current_user_can( 'manage_options' );
$is_instructor = \Academy\Helper::is_instructor_of_this_course( $student_id, $course_id );
$enrolled = \Academy\Helper::is_enrolled( $course_id, $student_id );
$response = [];
if ( $is_administrator || $is_instructor || $enrolled ) {
$analytics_data = \Academy\Helper::prepare_analytics_for_user( $student_id, $course_id );
$analytics_data['title'] = get_the_title( $course_id );
$analytics_data['course_link'] = get_post_permalink( $course_id );
// See the matching comment in enrolled-courses.php — empty for a
// normal enrollment, filled in by academy-pro's WooCommerce
// Subscriptions addon for a subscription-linked course.
$analytics_data['access_note'] = apply_filters( 'academy/templates/frontend-dashboard/course_access_note', '', $course_id, $student_id );
$response['enrolled_info'][] = $analytics_data;
}
wp_send_json_success(
apply_filters(
'academy/get_course_analytics',
$response,
$course_id
)
);
}
public function import_all_courses() {
check_admin_referer( 'academy_nonce', 'security' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_die();
}
if ( ! isset( $_FILES['upload_file'] ) ) {
wp_send_json_error( __( 'Upload File is empty.', 'academy' ) );
}
$file = $_FILES['upload_file']; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce checked by AbstractAjaxHandler; uploaded-file array, only the PHP tmp path is read.
if ( 'csv' !== pathinfo( $file['name'] )['extension'] ) {
wp_send_json_error( __( 'Wrong File Format! Please import csv file.', 'academy' ) );
}
$file_open = fopen( $file['tmp_name'], 'r' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fopen, WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- reads the PHP upload tmp file
if ( false !== $file_open ) {
$has_course = false;
$has_course_meta = false;
$has_quiz = false;
$has_question = false;
$has_answer = false;
$has_lesson = false;
$has_assignment = false;
$course_header = [];
$course_meta_header = [];
$lesson_header = [];
$assignment_header = [];
$quiz_header = [];
$new_course_id = 0;
$new_quiz_id = 0;
$new_question_id = 0;
$old_course_id = 0;
$course_ids = [];
$new_curr_item = [];
$response = [];
while ( true ) {
$item = fgetcsv( $file_open, null, ',', '"', '\\' );
if ( false === $item ) {
break;
}
$item = array_map( array( \Academy\Classes\ExportBase::class, 'csv_restore_cell' ), $item );
if ( in_array( 'post_title', $item, true ) ) {
$course_header = array_map( 'strtolower', $item );
$has_course = true;
$has_question = false;
$has_answer = false;
continue;
} elseif ( in_array( 'course_expire_enrollment', $item, true ) ) {
$course_meta_header = array_map( 'strtolower', $item );
$has_course_meta = true;
$has_question = false;
$has_answer = false;
continue;
} elseif ( in_array( 'lesson_title', $item, true ) ) {
$lesson_header = array_map( 'strtolower', $item );
$has_lesson = true;
$has_question = false;
$has_answer = false;
continue;
} elseif ( in_array( 'quiz_title', $item, true ) ) {
$quiz_header = array_map( 'strtolower', $item );
$has_quiz = true;
$has_question = false;
$has_answer = false;
continue;
} elseif ( in_array( 'question_title', $item, true ) ) {
$question_header = array_map( 'strtolower', $item );
$has_question = true;
$has_answer = false;
continue;
} elseif ( in_array( 'answer_title', $item, true ) ) {
$answer_header = array_map( 'strtolower', $item );
$has_answer = true;
continue;
} elseif ( in_array( 'assignment_title', $item, true ) ) {
$assignment_header = array_map( 'strtolower', $item );
$has_assignment = true;
$has_question = false;
$has_answer = false;
continue;
}//end if
if ( $has_course ) {
$has_course = false;
$item = self::combine_csv_item( $item, $course_header );
$course_item = array_combine( $course_header, array_pad( array_slice( $item, 0, count( $course_header ) ), count( $course_header ), '' ) );
if ( empty( $course_item['post_title'] ) ) {
$response[] = __( 'Empty Course data', 'academy' );
continue;
}
$slug_exist = \Academy\Helper::is_course_slug_exist( $course_item['post_title'] );
if ( $slug_exist ) {
$course = \Academy\Helper::get_page_by_title( $course_item['post_title'], 'academy_courses' );
$new_course_id = 0;
$old_course_id = $course->ID;
$course_ids[] = $old_course_id;
$response[] = __( 'Failed, Already Inserted the Course', 'academy' ) . ' - ' . $course_item['post_title'];
continue;
}
$old_course_id = 0;
$new_course_id = $this->course_data_set( $course_item );
if ( $new_course_id && ! empty( $course_item['course_thumbnail_url'] ) ) {
if ( ! \Academy\Classes\MediaImporter::set_post_thumbnail_from_url( $new_course_id, $course_item['course_thumbnail_url'] ) ) {
$response[] = __( 'Could not import the course thumbnail - ', 'academy' ) . $course_item['post_title'];
}
}
$response[] = $new_course_id ? __( 'Successfully Inserted the Course - ', 'academy' ) . $course_item['post_title'] : __( 'Sorry, Failed to Inserted the Course - ', 'academy' ) . $course_item['post_title'];
$course_ids[] = $new_course_id;
} elseif ( $has_course_meta && ( $new_course_id || $old_course_id ) ) {
$has_course_meta = false;
$item = self::combine_csv_item( $item, $course_meta_header );
$course_meta_item = array_combine( $course_meta_header, array_pad( array_slice( $item, 0, count( $course_meta_header ) ), count( $course_meta_header ), '' ) );
$meta = isset( $course_meta_item['course_curriculum'] ) ? json_decode( $course_meta_item['course_curriculum'], true ) : false;
$new_curr_item[] = [
'course_id' => $new_course_id ? $new_course_id : $old_course_id,
'curriculum' => $meta
];
if ( $new_course_id ) {
$this->insert_course_meta_value( $course_meta_item, $new_course_id );
}
} elseif ( $has_lesson ) {
$has_lesson = false;
$item = self::combine_csv_item( $item, $lesson_header );
$lesson_item = array_combine( $lesson_header, array_pad( array_slice( $item, 0, count( $lesson_header ) ), count( $lesson_header ), '' ) );
$exist_lesson = \Academy\Helper::get_lesson_by_title( $lesson_item['lesson_title'] );
if ( ! $exist_lesson ) {
$new_lesson_id = $this->insert_lesson_data( $lesson_item );
}
$response[] = ! empty( $new_lesson_id ) ? __( 'Successfully Inserted the Lesson - ', 'academy' ) . $lesson_item['lesson_title'] : __( 'Sorry, Already have the Lesson - ', 'academy' ) . $lesson_item['lesson_title'];
} elseif ( $has_quiz ) {
$has_quiz = false;
if ( ! \Academy\Helper::is_active_academy_pro() ) {
continue;
}
$item = self::combine_csv_item( $item, $quiz_header );
$quiz_item = array_combine( $quiz_header, array_pad( array_slice( $item, 0, count( $quiz_header ) ), count( $quiz_header ), '' ) );
$exist_quiz = \Academy\Helper::get_page_by_title( $quiz_item['quiz_title'], 'academy_quiz' );
if ( ! $exist_quiz ) {
$new_quiz_id = apply_filters( 'academy_pro/export-import/insert_quiz_data', $quiz_item );
}
$response[] = ! empty( $new_quiz_id ) ? __( 'Successfully Inserted the Quiz - ', 'academy' ) . $quiz_item['quiz_title'] : __( 'Sorry, Already have the Quiz - ', 'academy' ) . $quiz_item['quiz_title'];
} elseif ( $has_question && $new_quiz_id ) {
$has_question = false;
$item = self::combine_csv_item( $item, $question_header );
$question_item = array_combine( $question_header, array_pad( array_slice( $item, 0, count( $question_header ) ), count( $question_header ), '' ) );
$new_question_id = apply_filters( 'academy_pro/export-import/insert_question_data', $question_item, $new_quiz_id );
$response[] = $new_question_id ? __( 'Successfully Inserted the Question - ', 'academy' ) . $question_item['question_title'] : __( 'Sorry, Already have the Question - ', 'academy' ) . $question_item['question_title'];
} elseif ( $has_answer && $new_quiz_id && $new_question_id ) {
// Keep reading answers: the export writes one header per question, then one row per answer.
$item = self::combine_csv_item( $item, $answer_header );
$answer_item = array_combine( $answer_header, array_pad( array_slice( $item, 0, count( $answer_header ) ), count( $answer_header ), '' ) );
apply_filters( 'academy_pro/export-import/insert_answer_data', $answer_item, $new_quiz_id, $new_question_id );
} elseif ( $has_assignment ) {
$has_assignment = false;
if ( ! \Academy\Helper::is_active_academy_pro() ) {
continue;
}
$item = self::combine_csv_item( $item, $assignment_header );
$assignment_item = array_combine( $assignment_header, array_pad( array_slice( $item, 0, count( $assignment_header ) ), count( $assignment_header ), '' ) );
$exist_assignment = \Academy\Helper::get_page_by_title( $assignment_item['assignment_title'], 'academy_assignments' );
if ( ! $exist_assignment ) {
$new_assignment_id = apply_filters( 'academy_pro/export-import/insert_assignment_data', $assignment_item );
}
$response[] = ! empty( $new_assignment_id ) ? __( 'Successfully Inserted the Assignment - ', 'academy' ) . $assignment_item['assignment_title'] : __( 'Sorry, Already have the Assignment - ', 'academy' ) . $assignment_item['assignment_title'];
}//end if
}//end while
if ( count( $new_curr_item ) ) {
foreach ( $new_curr_item as $item ) {
$this->update_course_curriculum( $item['curriculum'], $item['course_id'] );
}
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose, WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closes the uploaded CSV opened with fopen() above
fclose( $file_open );
wp_send_json_success( $response );
}//end if
wp_send_json_error( $response );
}
public function course_data_set( $course_item ) {
if ( empty( $course_item ) ) {
return false;
}
$allowed_post_fields = [
'post_title' => 'sanitize_text_field',
'post_date' => 'sanitize_text_field',
'post_content' => 'wp_kses_post',
'post_excerpt' => 'sanitize_text_field',
'post_status' => 'sanitize_key',
'comment_status' => 'sanitize_key',
'post_name' => 'sanitize_title',
'post_parent' => 'absint',
'comment_count' => 'absint'
];
$post_data = [];
foreach ( $allowed_post_fields as $key => $sanitizer ) {
$post_data[ $key ] = isset( $course_item[ $key ] ) ? call_user_func( $sanitizer, $course_item[ $key ] ) : '';
}
$post_data['post_content'] = \Academy\Classes\MediaImporter::import_content_images( $post_data['post_content'] );
$user_id = get_current_user_id();
$post_data['post_type'] = 'academy_courses';
$post_data['post_author'] = $user_id;
$course_id = wp_insert_post( $post_data );
add_user_meta( $user_id, 'academy_instructor_course_id', $course_id );
return $course_id;
}
public function insert_course_meta_value( $course_item, $new_course_id ) {
$response = false;
$expire_enrollment = isset( $course_item['course_expire_enrollment'] ) ? sanitize_text_field( $course_item['course_expire_enrollment'] ) : 0;
$course_type = isset( $course_item['course_type'] ) ? sanitize_text_field( $course_item['course_type'] ) : 'free';
$students = isset( $course_item['course_max_students'] ) ? absint( $course_item['course_max_students'] ) : 0;
$course_language = isset( $course_item['course_language'] ) ? sanitize_text_field( $course_item['course_language'] ) : '';
$difficulty_level = isset( $course_item['course_difficulty_level'] ) ? sanitize_text_field( $course_item['course_difficulty_level'] ) : 'beginner';
$course_benefits = isset( $course_item['course_benefits'] ) ? sanitize_text_field( $course_item['course_benefits'] ) : '';
$course_requirements = isset( $course_item['course_requirements'] ) ? sanitize_text_field( $course_item['course_requirements'] ) : '';
$course_audience = isset( $course_item['course_audience'] ) ? sanitize_text_field( $course_item['course_audience'] ) : 'beginner';
$course_materials = isset( $course_item['course_materials'] ) ? sanitize_text_field( $course_item['course_materials'] ) : '';
$course_qa = isset( $course_item['is_enabled_course_qa'] ) ? sanitize_key( $course_item['is_enabled_course_qa'] ) : false;
$course_announcement = isset( $course_item['is_enabled_course_announcements'] ) ? sanitize_key( $course_item['is_enabled_course_announcements'] ) : false;
$course_duration = isset( $course_item['course_duration'] ) ? json_decode( sanitize_text_field( $course_item['course_duration'] ), true ) : array( 0, 0, 0 );
$intro_video = isset( $course_item['course_intro_video'] ) ? json_decode( sanitize_text_field( $course_item['course_intro_video'] ), true ) : array();
$course_curriculum = isset( $course_item['course_curriculum'] ) ? json_decode( sanitize_text_field( $course_item['course_curriculum'] ), true ) : array();
$certificate_id = 0;
$product_id = isset( $course_item['course_product_id'] ) ? absint( $course_item['course_product_id'] ) : 0;
$download_id = isset( $course_item['course_download_id'] ) ? absint( $course_item['course_download_id'] ) : 0;
$course_review = isset( $course_item['is_disabled_course_review'] ) ? sanitize_key( $course_item['is_disabled_course_review'] ) : false;
$rcp_membership_levels = isset( $course_item['rcp_membership_levels'] ) ? json_decode( sanitize_text_field( $course_item['rcp_membership_levels'] ), true ) : array();
$enable_certificate = isset( $course_item['course_enable_certificate'] ) ? sanitize_key( $course_item['course_enable_certificate'] ) : false;
$meta_args = [
'academy_course_expire_enrollment' => $expire_enrollment,
'academy_course_type' => $course_type,
'academy_course_max_students' => $students,
'academy_course_language' => $course_language,
'academy_course_difficulty_level' => $difficulty_level,
'academy_course_benefits' => $course_benefits,
'academy_course_requirements' => $course_requirements,
'academy_course_audience' => $course_audience,
'academy_course_materials_included' => $course_materials,
'academy_is_enabled_course_qa' => $course_qa,
'academy_is_enabled_course_announcements' => $course_announcement,
'academy_course_duration' => $course_duration,
'academy_course_intro_video' => $intro_video,
'academy_course_curriculum' => $course_curriculum,
'academy_course_certificate_id' => $certificate_id,
'academy_is_disabled_course_review' => $course_review,
'academy_rcp_membership_levels' => $rcp_membership_levels,
'academy_course_enable_certificate' => $enable_certificate,
];
// add default product id
add_post_meta( $new_course_id, 'academy_course_download_id', 0 );
add_post_meta( $new_course_id, 'academy_course_product_id', 0 );
// Handle WooCommerce product creation
if ( $product_id && \Academy\Helper::get_addon_active_status( 'woocommerce' ) ) {
$product = new \WC_Product_Simple( $product_id );
$product->set_name( get_the_title( $new_course_id ) );
$product->set_slug( get_post_field( 'post_name', $new_course_id ) );
$product->set_regular_price( absint( $course_item['regular_price'] ) ?? 0 );
if ( ! empty( $course_item['sale_price'] ) ) {
$product->set_sale_price( absint( $course_item['sale_price'] ) );
}
$product_id = $product->save();
if ( $product_id ) {
update_post_meta( $product_id, '_academy_product', 'yes' );
update_post_meta( $new_course_id, 'academy_course_product_id', $product_id );
}
}
// Handle EDD download creation
if ( $download_id && \Academy\Helper::get_addon_active_status( 'easy-digital-downloads' ) ) {
$args = [
'post_type' => 'download',
'post_status' => 'publish',
'post_title' => get_the_title( $new_course_id ),
];
$download_id = wp_insert_post( $args, true );
if ( ! is_wp_error( $download_id ) ) {
$download = new \EDD_Download( $download_id );
update_post_meta( $download_id, '_academy_course', 'yes' );
update_post_meta( $download_id, 'edd_price', absint( $course_item['edd_price'] ) ?? 0 );
update_post_meta( $new_course_id, 'academy_course_download_id', $download_id );
}
}
// Insert course meta
foreach ( $meta_args as $key => $value ) {
$response = true;
add_post_meta( $new_course_id, $key, $value );
}
return $response;
}
public function insert_lesson_data( $item ) {
if ( empty( $item['lesson_title'] ) ) {
return '';
}
$user = get_user_by( 'login', $item['lesson_author'] );
$allowed_tags = wp_kses_allowed_html( 'post' );
$allowed_tags['input'] = [
'type' => true,
'name' => true,
'value' => true,
'class' => true,
];
$allowed_tags['form'] = [
'action' => true,
'method' => true,
'class' => true,
];
$allowed_tags['iframe'] = [
'src' => true,
'width' => true,
'height' => true,
'frameborder' => true,
'allow' => true,
'allowfullscreen' => true,
];
$content = \Academy\Classes\MediaImporter::import_content_images( wp_kses( $item['lesson_content'], $allowed_tags ) );
try {
$lesson = LessonApi::create(
[
'lesson_author' => $user ? $user->ID : (int) get_current_user_id(),
'lesson_title' => sanitize_text_field( $item['lesson_title'] ),
'lesson_name' => \Academy\Helper::generate_unique_lesson_slug( $item['lesson_title'] ),
'lesson_content' => $content,
'lesson_status' => $item['lesson_status'],
],
[
'featured_media' => \Academy\Classes\MediaImporter::import_image_from_url( $item['lesson_featured_media_url'] ?? '' ),
'attachment' => 0,
'is_previewable' => sanitize_text_field( $item['is_previewable'] ),
'video_duration' => sanitize_text_field( $item['video_duration'] ),
'video_source' => [
'type' => sanitize_text_field( $item['video_source_type'] ),
'url' => $this->sanitize_video_source( $item['video_source_type'], $item['video_source_url'] ),
],
]
);
$lesson->save();
return $lesson->id();
} catch ( Throwable $e ) {
// Return false on failure
return false;
}//end try
}
public function sanitize_video_source( $source, $url ) {
switch ( $source ) {
case 'embedded':
return filter_var( $url, FILTER_SANITIZE_URL );
case 'short_code':
return \Academy\Helper::sanitize_video_shortcode( wp_unslash( $url ) );
default:
return sanitize_text_field( $url );
}
}
public function update_course_curriculum( $curriculums, $new_course_id ) {
if ( is_array( $curriculums ) ) {
$new_curriculum = array();
foreach ( $curriculums as $curriculum ) {
$new_topics = array();
foreach ( $curriculum['topics'] as $topic ) {
if ( isset( $topic['type'] ) && 'sub-curriculum' !== $topic['type'] ) {
$new_topics[] = $this->set_topics( $topic );
} elseif ( ! empty( $topic ) ) {
$sub_topics = [
'name' => $topic['name'],
'type' => $topic['type'],
'id' => $topic['id'],
'topics' => [],
];
foreach ( $topic['topics'] as $sub_topic ) {
$sub_topics['topics'][] = $this->set_topics( $sub_topic );
}
$new_topics[] = $sub_topics;
}
}//end foreach
$new_curriculum[] = array(
'title' => (string) $curriculum['title'] ? $curriculum['title'] : 'Academy Topics',
'content' => $curriculum['content'],
'topics' => $new_topics,
);
}//end foreach
if ( is_array( $new_curriculum ) ) {
update_post_meta( $new_course_id, 'academy_course_curriculum', $new_curriculum );
}
}//end if
}
private function set_topics( $topic ) {
switch ( $topic['type'] ) {
case 'lesson':
$lesson = \Academy\Helper::get_lesson_by_title( $topic['name'] );
if ( $lesson ) {
return array(
'id' => $lesson['ID'],
'name' => $topic['name'],
'type' => 'lesson',
);
}
break;
case 'quiz':
$quiz = \Academy\Helper::get_page_by_title( $topic['name'], 'academy_quiz' );
if ( $quiz ) {
return array(
'id' => $quiz->ID,
'name' => $topic['name'],
'type' => 'quiz',
);
}
break;
case 'assignment':
$assignment = \Academy\Helper::get_page_by_title( $topic['name'], 'academy_assignments' );
if ( $assignment ) {
return array(
'id' => $assignment->ID,
'name' => $topic['name'],
'type' => 'assignment',
);
}
break;
case 'quizpress_quiz':
$quizpress_quiz = \Academy\Helper::get_page_by_title( $topic['name'], 'quizpress_quiz' );
if ( $quizpress_quiz ) {
return array(
'id' => $quizpress_quiz->ID,
'name' => $topic['name'],
'type' => 'quizpress_quiz',
);
}
break;
}//end switch
}
public function combine_csv_item( $item, $course_header ) {
// Validate before combine
$header_count = count( $course_header );
$row_count = count( $item );
if ( $row_count !== $header_count ) {
// Fix mismatch (pad or trim)
if ( $row_count < $header_count ) {
$item = array_pad( $item, $header_count, '' );
} else {
$item = array_slice( $item, 0, $header_count );
}
}
return $item;
}
public function save_youtube_api_key( $payload_data ) {
$api_key = ! empty( $payload_data['api_key'] ) ? sanitize_text_field( $payload_data['api_key'] ) : '';
if ( empty( $api_key ) ) {
wp_send_json_error( esc_html__( 'YouTube API Key is required.', 'academy' ) );
}
$url = "https://www.googleapis.com/youtube/v3/videos?part=id&id=dQw4w9WgXcQ&key={$api_key}";
$response = wp_safe_remote_get( $url, [ 'timeout' => 3 ] );
if ( is_wp_error( $response ) ) {
wp_send_json_error( esc_html__( 'Failed to validate API key. Please try again.', 'academy' ) );
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( empty( $data ) || isset( $data['error'] ) ) {
wp_send_json_error( esc_html__( 'Invalid YouTube API Key.', 'academy' ) );
}
$user_id = get_current_user_id();
$existing_key = get_user_meta( $user_id, 'academy_youtube_api_key', true );
if ( ! $existing_key ) {
add_user_meta( $user_id, 'academy_youtube_api_key', $api_key );
wp_send_json_success( esc_html__( 'Successfully save your YouTube API Key.', 'academy' ) );
}
update_user_meta( $user_id, 'academy_youtube_api_key', $api_key );
wp_send_json_success( esc_html__( 'Successfully update your YouTube API Key.', 'academy' ) );
}
public function import_from_playlist( $payload_data ): void {
$payload = Sanitizer::sanitize_payload([
'playlist_url' => 'string',
'course_status' => 'string',
'course_type' => 'string',
], $payload_data );
$url = isset( $payload['playlist_url'] ) ? $payload['playlist_url'] : '';
if ( empty( $url ) ) {
wp_send_json_error( esc_html__( 'Playlist url field is required.', 'academy' ) );
}
$user_id = get_current_user_id();
$api = sanitize_text_field( get_user_meta( $user_id, 'academy_youtube_api_key', true ) );
$parsed_url = wp_parse_url( $url );
switch ( str_replace( 'www.', '', $parsed_url['host'] ?? '' ) ) {
case 'youtube.com':
if ( empty( $api ) ) {
wp_send_json_error( esc_html__( 'Youtube API key is missing.', 'academy' ) );
}
parse_str( isset( $parsed_url['query'] ) ? $parsed_url['query'] : '', $query_arr );
$playlist_id = ( $query_arr['list'] ?? '' );
if ( empty( $playlist_id ) ) {
wp_send_json_error( esc_html__( 'Not a valid playlist url.', 'academy' ) );
}
$course_id = ( new Import(
new Info( $api, $playlist_id, Youtube::class ),
$payload['course_status'] ?? '',
$payload['course_type'] ?? ''
) )->run();
if ( $course_id ) {
add_user_meta( $user_id, 'academy_instructor_course_id', $course_id );
wp_send_json_success([
'message' => esc_html__( 'Course Created!', 'academy' ),
'course_id' => $course_id
]);
}
break;
default:
wp_send_json_error( esc_html__( 'Not a valid playlist url.', 'academy' ) );
break;
}//end switch
}
}