<?php
/**
* Course Progress Functions
*
* @since 2.1.0
*
* @package LearnDash\Course
*/
use LearnDash\Core\Utilities\Cast;
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
// cspell:ignore accessable .
/**
* Outputs the HTML output to mark a course complete.
*
* Must meet requirements of course to mark the course as complete.
*
* @since 2.1.0
*
* @param WP_Post $post The `WP_Post` lesson or topic object.
* @param array $atts Optional. An array of attributes to mark course complete. Default empty array.
*
* @return string HTML output to mark course complete
*/
function learndash_mark_complete( $post, $atts = array() ) {
if ( ! is_user_logged_in() ) {
return '';
}
$user_id = get_current_user_id();
if ( isset( $_POST['sfwd_mark_complete'] ) && isset( $_POST['post'] ) && intval( $_POST['post'] ) == $post->ID ) {
return '';
}
$bypass_course_limits_admin_users = learndash_can_user_bypass( $user_id, 'learndash_course_progression', $post->ID, $post );
/**
* Bypass prerequisites.
*
* @since 2.4.0
*
* @param boolean $bypass True/False is user is allowed to bypass.
* @param integer $user_id User ID.
* @param integer $post_id Post ID.
* @param object $post WP_Post instance
*/
$bypass_course_limits_admin_users = apply_filters( 'learndash_prerequities_bypass', $bypass_course_limits_admin_users, $user_id, $post->ID, $post ); // cspell:disable-line -- prerequities are prerequisites...
$course_id = learndash_get_course_id( $post->ID );
if ( ( learndash_lesson_progression_enabled() ) && ( ! $bypass_course_limits_admin_users ) ) {
if ( ! sfwd_lms_has_access( $course_id, $user_id ) ) {
return '';
}
// Check Course Prerequisites.
if ( ! learndash_is_course_prerequities_completed( $course_id, $user_id ) ) { // cspell:disable-line -- prerequities are prerequisites...
return '';
}
if ( ! learndash_check_user_course_points_access( $course_id, $user_id ) ) {
return '';
}
$step_quiz_list = learndash_get_lesson_quiz_list( $post->ID, $user_id, $course_id );
if ( ! empty( $step_quiz_list ) ) {
foreach ( $step_quiz_list as $quiz ) {
if ( 'notcompleted' === $quiz['status'] ) {
return '';
}
}
}
if ( 'sfwd-lessons' === $post->post_type ) {
$progress = learndash_get_course_progress( $user_id, $post->ID );
if ( ! empty( $progress['this']->completed ) ) {
/** This filter is documented in includes/class-ld-cpt-instance.php */
if ( ! apply_filters( 'learndash_previous_step_completed', false, $progress['this']->ID, $user_id ) ) {
return learndash_show_mark_incomplete( $post, $atts );
}
}
if ( ! empty( $progress['prev'] ) && empty( $progress['prev']->completed ) && learndash_lesson_progression_enabled() ) {
/** This filter is documented in includes/class-ld-cpt-instance.php */
if ( ! apply_filters( 'learndash_previous_step_completed', false, $progress['prev']->ID, $user_id ) ) {
return '';
}
}
if ( ! learndash_lesson_topics_completed( $post->ID ) ) {
/** This filter is documented in includes/class-ld-cpt-instance.php */
if ( ! apply_filters( 'learndash_previous_step_completed', false, $post->ID, $user_id ) ) {
return '';
}
}
}
if ( 'sfwd-topic' === $post->post_type ) {
$progress = learndash_get_course_progress( $user_id, $post->ID );
if ( ! empty( $progress['this']->completed ) ) {
/** This filter is documented in includes/class-ld-cpt-instance.php */
if ( ! apply_filters( 'learndash_previous_step_completed', false, $progress['this']->ID, $user_id ) ) {
return learndash_show_mark_incomplete( $post, $atts );
}
}
if ( ! empty( $progress['prev'] ) && empty( $progress['prev']->completed ) && learndash_lesson_progression_enabled() ) {
/** This filter is documented in includes/class-ld-cpt-instance.php */
if ( ! apply_filters( 'learndash_previous_step_completed', false, $progress['prev']->ID, $user_id ) ) {
return '';
}
}
if ( learndash_lesson_progression_enabled() ) {
if ( LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Courses_Builder', 'shared_steps' ) == 'yes' ) {
$lesson_id = learndash_course_get_single_parent_step( $course_id, $post->ID );
} else {
$lesson_id = learndash_get_setting( $post, 'lesson' );
}
$lesson = get_post( $lesson_id );
if ( ! learndash_is_previous_complete( $lesson ) ) {
/** This filter is documented in includes/class-ld-cpt-instance.php */
if ( ! apply_filters( 'learndash_previous_step_completed', false, $lesson->ID, $user_id ) ) {
return '';
}
}
}
}
$previous_item_id = learndash_user_progress_get_previous_incomplete_step( $user_id, $course_id, $post->ID );
if ( ! empty( $previous_item_id ) && ( $previous_item_id !== $post->ID ) ) {
return '';
}
} else {
$progress = learndash_get_course_progress( $user_id, $post->ID );
if ( ! empty( $progress['this']->completed ) ) {
return '';
}
}
if ( learndash_lesson_hasassignments( $post ) ) { // cspell:disable-line.
global $learndash_assignment_upload_message;
$ret = SFWD_LMS::get_template(
'learndash_lesson_assignment_upload_form.php',
array(
'course_step_post' => $post,
'user_id' => $user_id,
'assignment_upload_error_message' => $learndash_assignment_upload_message,
)
);
if ( ! is_null( $ret ) ) {
return $ret;
}
if ( ( ! learndash_is_admin_user( $user_id ) ) || ( ! $bypass_course_limits_admin_users ) ) {
$assignments = learndash_get_user_assignments( $post->ID, $user_id, $course_id );
if ( empty( $assignments ) ) {
return false;
} else {
foreach ( $assignments as $assignment ) {
if ( ! learndash_is_assignment_approved_by_meta( $assignment->ID ) ) {
return false;
}
}
}
}
}
$return = '';
$button_disabled = '';
$time = 0;
$time_value = learndash_forced_lesson_time( $post );
if ( ! empty( $time_value ) ) {
$time = learndash_convert_lesson_time_time( $time_value );
}
if ( ( ! learndash_is_admin_user( $user_id ) ) || ( ! $bypass_course_limits_admin_users ) ) {
if ( ! empty( $time ) ) {
$time_cookie_key = learndash_forced_lesson_time_cookie_key( $post );
if ( ! empty( $time_cookie_key ) ) {
/**
* Note this is not a 100% check. We are only checking if the cookie
* key exists and is zero. But this cookie could have been set from
* external.
*/
if ( ( isset( $_COOKIE[ 'learndash_timer_cookie_' . $time_cookie_key ] ) ) && ( '0' === $_COOKIE[ 'learndash_timer_cookie_' . $time_cookie_key ] ) ) {
$time = 0;
}
}
if ( ! empty( $time ) ) {
// Set the mark complete button disabled.
$button_disabled = " disabled='disabled' ";
wp_enqueue_script(
'jquery-cookie',
plugins_url( 'js/jquery.cookie' . learndash_min_asset() . '.js', WPPROQUIZ_FILE ),
array( 'jquery' ),
'1.4.0',
true
);
global $learndash_assets_loaded;
$learndash_assets_loaded['scripts']['jquery-cookie'] = __FUNCTION__;
}
}
}
/**
* Filters attributes for mark a course complete form.
*
* @since 3.0.0
*
* @param array $attributes An array of form, button, and timer attributes to override id and class.
* @param WP_Post $post WP_Post object being displayed.
*/
$atts = apply_filters( 'learndash_mark_complete_form_atts', $atts, $post );
if ( isset( $atts['form']['id'] ) ) {
$form_id = ' id="' . esc_attr( $atts['form']['id'] ) . '" ';
} else {
$form_id = '';
}
if ( isset( $atts['form']['class'] ) ) {
$form_class = ' class="sfwd-mark-complete ' . esc_attr( $atts['form']['class'] ) . '" ';
} else {
$form_class = ' class="sfwd-mark-complete" ';
}
if ( isset( $atts['button']['id'] ) ) {
$button_id = ' id="' . esc_attr( $atts['button']['id'] ) . '" ';
} else {
$button_id = '';
}
if ( isset( $atts['button']['class'] ) ) {
$button_class = ' class="learndash_mark_complete_button ' . esc_attr( $atts['button']['class'] ) . '" ';
} else {
$button_class = ' class="learndash_mark_complete_button" ';
}
$button_label = learndash_course_steps_is_external( $post->ID ) && ! learndash_course_steps_is_external_attendance_required( $post->ID )
? LearnDash_Custom_Label::get_label( LearnDash_Custom_Label::$button_skip_event )
: LearnDash_Custom_Label::get_label( 'button_mark_complete' );
$form_fields = '<input type="hidden" value="' . $post->ID . '" name="post" />
<input type="hidden" value="' . learndash_get_course_id( $post->ID ) . '" name="course_id" />
<input type="hidden" value="' . wp_create_nonce( 'sfwd_mark_complete_' . get_current_user_id() . '_' . $post->ID ) . '" name="sfwd_mark_complete" />
<input type="submit" ' . $button_id . ' value="' . $button_label . '" ' . $button_class . ' ' . $button_disabled . '/>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Method escapes output
/**
* Filters form fields to mark a course complete.
*
* @since 3.0.0
*
* @param string $form_fields HTML output for course mark complete form fields.
* @param WP_Post $post WP_Post object being displayed.
*/
$form_fields = apply_filters( 'learndash_mark_complete_form_fields', $form_fields, $post );
$return .= '<form ' . $form_id . ' ' . $form_class . ' method="post" action="">' . $form_fields . '</form>';
if ( ( ! learndash_is_admin_user( $user_id ) ) || ( ! $bypass_course_limits_admin_users ) ) {
if ( ! empty( $time ) ) {
if ( isset( $atts['timer']['id'] ) ) {
$timer_id = ' id="' . esc_attr( $atts['timer']['id'] ) . '" ';
} else {
$timer_id = '';
}
$timer_class = ' class="learndash_timer';
if ( isset( $atts['timer']['class'] ) ) {
$timer_class .= ' ' . esc_attr( $atts['timer']['class'] );
}
$timer_class .= '" ';
$return .= '<span ' . $timer_id . ' ' . $timer_class . ' data-timer-seconds="' . $time . '" data-button="input.learndash_mark_complete_button" data-cookie-key="' . $time_cookie_key . '"></span>';
}
}
/**
* Filters HTML output to mark a course completion.
*
* @since 2.1.0
*
* @param string $return HTML output to mark course complete.
* @param WP_Post $post WP_Post object being displayed.
*/
return apply_filters( 'learndash_mark_complete', $return, $post );
}
/**
* Checks if the lesson topics are completed.
*
* @since 2.1.0
*
* @param int $lesson_id Lesson ID.
* @param boolean $mark_lesson_complete Optional. Whether to mark the lesson complete. Default false.
*
* @return boolean Returns true if the lesson is completed otherwise false.
*/
function learndash_lesson_topics_completed( $lesson_id, $mark_lesson_complete = false ) {
$topics = learndash_get_topic_list( $lesson_id );
if ( empty( $topics[0]->ID ) ) {
return true;
}
$progress = learndash_get_course_progress( null, $topics[0]->ID );
if ( empty( $progress['posts'] ) || ! is_array( $progress['posts'] ) ) {
return false;
}
foreach ( $progress['posts'] as $topic ) {
if ( empty( $topic->completed ) ) {
return false;
}
}
if ( $mark_lesson_complete ) {
$user_id = get_current_user_id();
learndash_process_mark_complete( null, $lesson_id );
}
return true;
}
/**
* Processes a request to mark a step/course complete.
*
* @since 2.1.0
* @since 4.11.0 Removed the $post argument.
*
* @return void
*/
function learndash_mark_complete_process() {
if (
! isset( $_POST['sfwd_mark_complete'] )
|| empty( $_POST['post'] )
) {
return;
}
$post_id = Cast::to_int(
sanitize_key( $_POST['post'] ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checked below.
);
$course_id = isset( $_POST['course_id'] )
? sanitize_key( $_POST['course_id'] )
: learndash_get_course_id( $post_id );
$course_id = Cast::to_int( $course_id );
$user_id = isset( $_POST['userid'] )
? Cast::to_int( sanitize_key( $_POST['userid'] ) ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checked below.
: get_current_user_id();
if (
$post_id <= 0
|| $course_id <= 0
|| $user_id <= 0
) {
return;
}
if (
! wp_verify_nonce(
sanitize_key( $_POST['sfwd_mark_complete'] ),
'sfwd_mark_complete_' . $user_id . '_' . $post_id
)
) {
return;
}
// Mark the step as completed.
$marked_completed = learndash_process_mark_complete( $user_id, $post_id, false, $course_id );
if ( ! $marked_completed ) {
return;
}
// Remove needed cookies.
// Remove the lesson/topic video progress cookie once the step is completed.
learndash_video_delete_cookie_for_step( $post_id, $course_id, $user_id );
// Remove the lesson/topic timer cookie once the step is completed.
$timer_cookie_key = learndash_forced_lesson_time_cookie_key( $post_id );
if ( ! empty( $timer_cookie_key ) ) {
if ( isset( $_COOKIE[ 'learndash_timer_cookie_' . $timer_cookie_key ] ) ) {
unset( $_COOKIE[ 'learndash_timer_cookie_' . $timer_cookie_key ] );
}
// empty value and expiration one hour before.
setcookie( 'learndash_timer_cookie_' . $timer_cookie_key, '', time() - 3600 );
}
learndash_safe_redirect(
learndash_course_get_step_completion_url( $post_id, $course_id, $user_id )
);
}
add_action( 'wp', 'learndash_mark_complete_process' );
/**
* Gets the course permalink.
*
* @since 2.1.0
*
* @param int|null $id Optional. The ID of the resource like course, topic, lesson, quiz, etc. Default null.
*
* @return string The course permalink.
*/
function learndash_get_course_url( $id = null ) {
if ( empty( $id ) ) {
$id = learndash_get_course_id();
}
return get_permalink( $id );
}
/**
* Redirects the user after quiz completion when a user clicks on the "continue" button.
* It redirects to the next step if the quiz is completed otherwise redirects back to the quiz page.
*
* @since 2.1.0
*
* @return void
*/
function learndash_quiz_redirect() {
if (
! isset( $_GET['quiz_redirect'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce here.
|| ! isset( $_GET['quiz_id'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce here.
) {
return;
}
$user_id = get_current_user_id();
$quiz_id = Cast::to_int( sanitize_key( $_GET['quiz_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce here.
if (
$user_id <= 0
|| $quiz_id <= 0
) {
return;
}
$course_id = Cast::to_int( learndash_get_course_id( $quiz_id ) );
if ( $course_id <= 0 ) {
return;
}
$redirect_url = learndash_is_quiz_complete( $user_id, $quiz_id, $course_id )
? learndash_course_get_step_completion_url( $quiz_id, $course_id, $user_id )
: Cast::to_string( learndash_get_step_permalink( $quiz_id, $course_id ) ); // Back to the quiz page.
learndash_safe_redirect( $redirect_url );
}
add_action( 'wp', 'learndash_quiz_redirect' );
/**
* Checks whether a user can attempt the quiz again.
*
* @since 2.1.0
*
* @param int $user_id User ID.
* @param int $quiz_id Quiz ID.
*
* @return boolean Returns true if the user can attempt quiz again otherwise false.
*/
function learndash_can_attempt_again( $user_id, $quiz_id ) {
$quizmeta = get_post_meta( $quiz_id, '_sfwd-quiz', true );
if ( isset( $quizmeta['sfwd-quiz_repeats'] ) ) {
$repeats = $quizmeta['sfwd-quiz_repeats'];
} else {
$repeats = '';
}
/**
* Filters number of quiz attempts allowed for a user.
*
* @param int $repeats Number of quiz attempts allowed.
* @param int $user_id User ID.
* @param int $quiz_id Quiz ID.
*/
$repeats = apply_filters( 'learndash_allowed_repeats', $repeats, $user_id, $quiz_id );
if ( '' == $repeats ) {
return true;
}
$quiz_results = get_user_meta( $user_id, '_sfwd-quizzes', true );
$count = 0;
if ( ! empty( $quiz_results ) ) {
foreach ( $quiz_results as $quiz ) {
if ( $quiz['quiz'] == $quiz_id ) {
$count++;
}
}
}
if ( $repeats > $count - 1 ) {
return true;
} else {
return false;
}
}
/**
* Checks if the previous topic or lesson is complete.
*
* @since 3.4.0
*
* @param WP_Post $post The `WP_Post` object of lesson or topic.
*
* @return int Returns 1 if the previous lesson or topic is completed otherwise 0.
*/
function learndash_is_previous_complete( $post ) {
$progress = learndash_get_course_progress( null, $post->ID );
if ( empty( $progress ) ) {
return 1;
}
if ( ! empty( $progress['prev'] ) && empty( $progress['prev']->completed ) ) {
return 0;
} else {
return 1;
}
}
/**
* Returns the previous lesson/topic to be completed.
*
* @since 2.3.0
*
* @param WP_Post $post The `WP_Post` object.
*
* @return WP_Post|null The `WP_Post` object of lesson/topic to be completed.
*/
function learndash_get_previous( $post ) {
$progress = learndash_get_course_progress( null, $post->ID );
if ( ! empty( $progress['prev'] ) ) {
return $progress['prev'];
}
return null;
}
/**
* Updates the user meta with completion status for any resource.
*
* @since 2.1.0
* @since 4.11.0 Added the $force parameter.
*
* @param int|null $user_id Optional. User ID. Default null.
* @param int|null $postid Optional. The ID of the resource like course, lesson, topic, etc. Default null.
* @param bool $onlycalculate Optional. Whether to mark the resource as complete. Default false.
* @param int $course_id Optional. Course ID. Default 0.
* @param bool $force Optional. Whether to force the completion when should not be completed.
* Default false.
*
* @return bool Returns true if the meta is updated successfully, otherwise false.
*/
function learndash_process_mark_complete( $user_id = null, $postid = null, $onlycalculate = false, $course_id = 0, $force = false ) {
if ( ( defined( 'LEARNDASH_COURSE_FUNCTIONS_LEGACY' ) ) && ( true === LEARNDASH_COURSE_FUNCTIONS_LEGACY ) ) {
return learndash_process_mark_complete_legacy( $user_id, $postid, $onlycalculate, $course_id );
}
if ( empty( $user_id ) ) {
if ( is_user_logged_in() ) {
$current_user = wp_get_current_user();
$user_id = $current_user->ID;
} else {
return false;
}
} else {
$current_user = get_user_by( 'id', $user_id );
}
$post = get_post( $postid );
if ( ! ( $post instanceof WP_Post ) ) {
return false;
}
// Allow admin users to bypass the validation.
$force = $force || learndash_can_user_bypass( $user_id, 'learndash_course_progression' );
// Check if the user can complete the step.
if (
! $force &&
! learndash_can_complete_step( $user_id, $post->ID, $course_id )
) {
return false;
}
/**
* In case of video progression, we need to check if the video is completed.
*
* This check should be done in the `learndash_can_complete_step` function, but this function is used in other places with a different purpose.
* We needed to add a new parameter to the function, making it weirder. So, putting the check here for now until we refactor this logic.
*/
$is_video_enabled = learndash_get_setting( $post->ID, 'lesson_video_enabled' ) === 'on'
&& ! empty( learndash_get_setting( $post->ID, 'lesson_video_url' ) );
if (
! $force
&& $is_video_enabled
&& ! learndash_video_complete_for_step( $post->ID, $course_id, $user_id )
) {
return false;
}
if ( ! $onlycalculate ) {
/**
* Filters whether to mark a process complete.
*
* @since 2.1.0
*
* @param boolean $mark_complete Whether to mark a process complete.
* @param WP_Post $post WP_Post object to be checked.
* @param WP_User $current_user Current logged in WP_User object.
*/
$process_completion = apply_filters( 'learndash_process_mark_complete', true, $post, $current_user );
if ( ! $process_completion ) {
return false;
}
}
if ( empty( $course_id ) ) {
$course_id = learndash_get_course_id( $postid );
}
$course_id = Cast::to_int( $course_id );
if ( empty( $course_id ) ) {
return false;
}
// Get the lesson ID.
$lesson_id = 0;
if ( LDLMS_Post_Types::get_post_type_slug( LDLMS_Post_Types::TOPIC ) === $post->post_type ) {
$lesson_id = learndash_is_course_builder_enabled()
? learndash_course_get_single_parent_step( $course_id, $post->ID )
: learndash_get_setting( $post, 'lesson' );
}
// Forcing quiz completion.
if ( LDLMS_Post_Types::get_post_type_slug( LDLMS_Post_Types::QUIZ ) === $post->post_type ) {
learndash_process_user_course_progress_update(
$user_id,
[
'quiz' => [
$course_id => [
$postid => 1,
],
],
],
$force
);
// We need to force a reload of the quiz progress object.
$quiz_progress_object = LDLMS_Factory_User::quiz_progress( $user_id );
if ( $quiz_progress_object instanceof LDLMS_Model_User_Quiz_Progress ) {
$quiz_progress_object->set_progress_unloaded();
}
}
$course_progress = learndash_user_get_course_progress( $user_id, $course_id, 'legacy' );
if ( ( empty( $course_progress ) ) || ( ! is_array( $course_progress ) ) ) {
$course_progress = array(
'lessons' => array(),
'topics' => array(),
);
}
if ( ( ! isset( $course_progress['lessons'] ) ) || ( empty( $course_progress['lessons'] ) ) ) {
$course_progress['lessons'] = array();
}
if ( ( ! isset( $course_progress['topics'] ) ) || ( empty( $course_progress['topics'] ) ) ) {
$course_progress['topics'] = array();
}
if ( 'sfwd-topic' === $post->post_type && empty( $course_progress['topics'][ $lesson_id ] ) ) {
$course_progress['topics'][ $lesson_id ] = array();
}
$lesson_completed = false;
$topic_completed = false;
if ( ! $onlycalculate && 'sfwd-lessons' === $post->post_type && empty( $course_progress['lessons'][ $postid ] ) ) {
$course_progress['lessons'][ $postid ] = 1;
$lesson_completed = true;
}
if ( ! $onlycalculate && 'sfwd-topic' === $post->post_type && empty( $course_progress['topics'][ $lesson_id ][ $postid ] ) ) {
$course_progress['topics'][ $lesson_id ][ $postid ] = 1;
$topic_completed = true;
}
$completed_old = isset( $course_progress['completed'] ) ? $course_progress['completed'] : 0;
$completed = learndash_course_get_completed_steps( $user_id, $course_id, $course_progress );
$course_progress['completed'] = $completed;
$course_progress['total'] = learndash_get_course_steps_count( $course_id );
/**
* Track the last post_id (Lesson, Topic, Quiz) seen by user.
*
* @since 2.1.0
*/
if ( in_array( $post->post_type, learndash_get_post_types( 'course_steps' ), true ) ) {
$course_progress['last_id'] = $post->ID;
}
$course_completed_time = time();
$course_earliest_completed_time = learndash_activity_course_get_earliest_started( $current_user->ID, $course_id, $course_completed_time );
// If course is completed.
if ( ( $course_progress['completed'] >= $completed_old ) && ( $course_progress['completed'] >= $course_progress['total'] ) ) {
/**
* Fires before the course is marked completed.
*
* @since 2.1.0
*
* @param array $course_data An array of course complete data.
*/
do_action(
'learndash_before_course_completed',
array(
'user' => $current_user,
'course' => get_post( $course_id ),
'progress' => array( $course_id => $course_progress ),
'completed_time' => $course_completed_time,
)
);
add_user_meta( $current_user->ID, 'course_completed_' . $course_id, $course_completed_time, true );
} else {
delete_user_meta( $current_user->ID, 'course_completed_' . $course_id );
}
learndash_user_set_course_progress( $user_id, $course_id, $course_progress );
if ( ! empty( $topic_completed ) ) {
$course_activity = learndash_activity_start_course( $current_user->ID, $course_id, $course_earliest_completed_time );
if ( $course_activity ) {
learndash_activity_update_meta_set(
$course_activity->activity_id,
array(
'steps_completed' => learndash_course_get_completed_steps( $current_user->ID, $course_id ),
'steps_last_id' => $post->ID,
)
);
}
learndash_activity_start_lesson( $current_user->ID, $course_id, $lesson_id, $course_completed_time );
learndash_activity_complete_topic( $current_user->ID, $course_id, $post->ID, $course_completed_time );
}
if ( ! empty( $lesson_completed ) ) {
$course_activity = learndash_activity_start_course( $current_user->ID, $course_id, $course_earliest_completed_time );
if ( $course_activity ) {
learndash_activity_update_meta_set(
$course_activity->activity_id,
array(
'steps_completed' => learndash_course_get_completed_steps( $current_user->ID, $course_id ),
'steps_last_id' => $post->ID,
)
);
}
learndash_activity_complete_lesson( $current_user->ID, $course_id, $post->ID, $course_completed_time );
}
$course_args = array(
'course_id' => $course_id,
'user_id' => $current_user->ID,
'post_id' => $course_id,
'activity_type' => 'course',
);
$course_activity = learndash_get_user_activity( $course_args );
if ( ! empty( $course_activity ) ) {
$course_activity = json_decode( wp_json_encode( $course_activity ), true );
} else {
$course_activity = $course_args;
}
if ( in_array( $post->post_type, learndash_get_post_types( 'course_steps' ), true ) ) {
$course_activity['activity_meta'] = array(
'steps_last_id' => $post->ID,
);
}
$do_course_complete_action = false;
if ( $course_progress['completed'] >= $completed_old && $course_progress['total'] == $course_progress['completed'] ) {
if ( ! $course_activity['activity_status'] ) {
$course_activity['activity_status'] = true;
$course_activity['activity_completed'] = $course_completed_time;
$course_activity['activity_updated'] = $course_completed_time;
if ( empty( $course_activity['activity_started'] ) ) {
$course_activity['activity_started'] = $course_earliest_completed_time;
}
$do_course_complete_action = true;
}
} else {
$course_activity['activity_completed'] = 0;
$course_activity['activity_status'] = false;
$course_activity['activity_updated'] = $course_completed_time;
}
learndash_update_user_activity( $course_activity );
$return = false;
if ( ! empty( $lesson_completed ) ) {
/**
* Fires after the lesson is marked completed.
*
* @since 2.1.0
*
* @param array $lesson_data An array of lesson complete data.
*/
do_action(
'learndash_lesson_completed',
array(
'user' => $current_user,
'course' => get_post( $course_id ),
'lesson' => $post,
'progress' => $course_progress,
)
);
$return = true;
}
if ( ! empty( $topic_completed ) ) {
/**
* Fires after the topic is marked completed.
*
* @since 2.1.0
*
* @param array $topic_data An array of topic complete data.
*/
do_action(
'learndash_topic_completed',
array(
'user' => $current_user,
'course' => get_post( $course_id ),
'lesson' => get_post( $lesson_id ),
'topic' => $post,
'progress' => $course_progress,
)
);
$return = true;
}
if ( true == $do_course_complete_action ) {
/**
* Fires after the course is marked completed.
*
* @since 2.1.0
*
* @param array $course_data An array of course complete data.
*/
do_action(
'learndash_course_completed',
array(
'user' => $current_user,
'course' => get_post( $course_id ),
'progress' => array( $course_id => $course_progress ),
'course_completed' => $course_completed_time,
)
);
set_transient(
'learndash_course_completed_' . $course_id . '_' . $user_id,
$course_completed_time,
HOUR_IN_SECONDS // We only need this for the redirect.
);
$return = true;
}
/**
* Try to complete the parent step if all the child steps are completed.
*
* The learndash_process_mark_complete validates special cases, for example, if the parent step requires a video to be completed.
*/
$parent_step_id = learndash_course_get_single_parent_step( $course_id, $post->ID );
if (
$parent_step_id > 0
&& empty( learndash_user_progression_get_incomplete_child_steps( $user_id, $course_id, $parent_step_id ) )
&& ! learndash_course_steps_is_external( $parent_step_id ) // Don't auto-complete external steps.
) {
learndash_process_mark_complete( $user_id, $parent_step_id, false, $course_id );
}
/**
* LEARNDASH-5883 - Always return true if we've made it this far.
*/
$return = true;
return $return;
}
/**
* Checks whether a quiz is complete for a user.
*
* @since 2.1.0
*
* @param int|null $user_id Optional. User ID. Default null.
* @param int $quiz_id Quiz ID.
* @param int $course_id Optional. Course ID. Default 0.
*
* @return boolean Returns true if the quiz is completed for a user otherwise false.
*/
function learndash_is_quiz_complete( $user_id = null, $quiz_id = 0, $course_id = 0 ) {
return ! learndash_is_quiz_notcomplete( $user_id, array( $quiz_id => 1 ), false, $course_id );
}
/**
* Checks whether a quiz is not completed for a user.
*
* Checks against quizzes in user meta and passing percentage of the quiz itself
*
* @since 2.1.0
* @since 2.3.1 Added $return_incomplete_quiz_ids parameter.
*
* @param int|null $user_id Optional. User ID for quizzes. Default null.
* @param array|null $quizzes Optional. Quiz ID to search user quizzes. Default null.
* @param boolean $return_incomplete_quiz_ids Optional. If true will return the array of incomplete quizzes. Default is false. Default false.
* @param int $course_id Optional. The Course ID to match. If -1 is passed then course match is not performed. Default 0.
*
* @return bool Returns true if the quiz(es) NOT complete otherwise false.
*/
function learndash_is_quiz_notcomplete( $user_id = null, $quizzes = null, $return_incomplete_quiz_ids = false, $course_id = 0 ) {
$user_id = (int) $user_id;
$course_id = (int) $course_id;
if ( empty( $user_id ) ) {
$user_id = get_current_user_id();
if ( empty( $user_id ) ) {
return true;
}
}
if ( ( is_null( $quizzes ) ) || ( ! is_array( $quizzes ) ) || ( empty( $quizzes ) ) ) {
return;
}
$quiz_results = get_user_meta( $user_id, '_sfwd-quizzes', true );
if ( ( ! empty( $quiz_results ) ) && ( is_array( $quiz_results ) ) ) {
foreach ( $quiz_results as $quiz ) {
if ( ( ! isset( $quiz['quiz'] ) ) || ( empty( $quiz['quiz'] ) ) ) {
continue;
}
if ( ! isset( $quizzes[ $quiz['quiz'] ] ) ) {
continue;
}
// Because we don't want to alter the original $course_id function param.
$quiz_course_id = $course_id;
if ( ( -1 !== $quiz_course_id ) && ( empty( $quiz_course_id ) ) ) {
$quiz_course_id = (int) learndash_get_course_id( intval( $quiz['quiz'] ) );
}
if ( ! isset( $quiz['course'] ) ) {
if ( ! learndash_is_course_shared_steps_enabled() ) {
/**
* If shared steps is not enabled we can determine the related
* course from the quiz post meta.
*/
$quiz['course'] = (int) learndash_get_setting( $quiz['quiz'], 'course' );
} else {
$quiz['course'] = learndash_get_course_id( $quiz['quiz'] );
}
}
$quiz['course'] = intval( $quiz['course'] );
$pass = false;
if ( ( -1 === $course_id ) || ( $course_id === $quiz['course'] ) ) {
if ( isset( $quiz['pass'] ) ) {
$pass = ( 1 == $quiz['pass'] ) ? 1 : 0;
} else {
$passingpercentage = (int) learndash_get_setting( $quiz['quiz'], 'passingpercentage' );
$pass = ( ! empty( $quiz['count'] ) && $quiz['score'] * 100 / $quiz['count'] >= $passingpercentage ) ? 1 : 0;
}
}
if ( $pass ) {
unset( $quizzes[ $quiz['quiz'] ] );
}
// Break if empty. No need to loop through the rest of user quiz progress.
if ( empty( $quizzes ) ) {
break;
}
}
}
if ( empty( $quizzes ) ) {
return 0;
} else {
if ( true == $return_incomplete_quiz_ids ) {
return $quizzes;
} else {
return 1;
}
}
}
/**
* Gets the user's current course progress.
*
* @since 2.1.0
*
* @param int|null $user_id Optional. User ID. Default null.
* @param int|null $postid Optional. Post ID. Default null.
* @param int|null $course_id Optional. Course ID. Default null.
*
* @return array An array of user's current course progress.
*/
function learndash_get_course_progress( $user_id = null, $postid = null, $course_id = null ) {
if ( ( defined( 'LEARNDASH_COURSE_FUNCTIONS_LEGACY' ) ) && ( true === LEARNDASH_COURSE_FUNCTIONS_LEGACY ) ) {
return learndash_get_course_progress_legacy( $user_id, $postid, $course_id );
}
$user_id = absint( $user_id );
$course_id = absint( $course_id );
if ( empty( $user_id ) ) {
$current_user = wp_get_current_user();
if ( empty( $current_user->ID ) ) {
return null;
}
$user_id = $current_user->ID;
}
$posts = array();
if ( empty( $course_id ) ) {
$course_id = learndash_get_course_id( $postid );
}
$this_post = get_post( $postid );
if ( ( ! $this_post ) || ( ! is_a( $this_post, 'WP_Post' ) ) ) {
return null;
}
$ld_course_object = LDLMS_Factory_Post::course( intval( $course_id ) );
if ( ! $ld_course_object ) {
return null;
}
$course_progress = learndash_user_get_course_progress( $user_id, $course_id, 'legacy' );
if ( empty( $course_progress ) ) {
$course_progress = array();
}
if ( 'sfwd-lessons' === $this_post->post_type ) {
$posts = $ld_course_object->get_lessons( array( 'nopaging' => true ) );
if ( empty( $course_progress ) || empty( $course_progress['lessons'] ) ) {
$completed_posts = array();
} else {
$completed_posts = $course_progress['lessons'];
}
} elseif ( 'sfwd-topic' === $this_post->post_type ) {
$lesson_id = learndash_course_get_single_parent_step( $course_id, $this_post->ID );
if ( ! $lesson_id ) {
return null;
}
$posts = $ld_course_object->get_topics( $lesson_id, array( 'nopaging' => true ) );
if ( empty( $course_progress ) || empty( $course_progress['topics'][ $lesson_id ] ) ) {
$completed_posts = array();
} else {
$completed_posts = $course_progress['topics'][ $lesson_id ];
}
}
$temp = '';
$prev_p = '';
$next_p = '';
$this_p = '';
if ( ! empty( $posts ) ) {
foreach ( $posts as $k => $post ) {
if ( $post instanceof WP_Post ) {
if ( ! empty( $completed_posts[ $post->ID ] ) ) {
$posts[ $k ]->completed = 1;
} else {
$posts[ $k ]->completed = 0;
}
if ( $post->ID == $postid ) {
$this_p = $post;
$prev_p = $temp;
}
if ( ! empty( $temp->ID ) && $temp->ID == $postid ) {
$next_p = $post;
}
$temp = $post;
}
}
} else {
$posts = array();
}
return array(
'posts' => $posts,
'this' => $this_p,
'prev' => $prev_p,
'next' => $next_p,
);
}
/**
* Checks if a lesson is complete.
*
* @since 2.1.0
*
* @param int|null $user_id Optional. User ID. Defaults to the current logged-in user. Default null.
* @param int $lesson_id Lesson ID.
* @param int $course_id Optional. Course ID. Default 0.
*
* @return boolean Return true if the lesson is complete otherwise false.
*/
function learndash_is_lesson_complete( $user_id = null, $lesson_id = 0, $course_id = 0 ) {
return ! learndash_is_lesson_notcomplete( $user_id, array( $lesson_id => 1 ), $course_id );
}
/**
* Checks if a lesson is not complete.
*
* @since 2.1.0
*
* @param int|null $user_id Optional. User ID. Defaults to the current logged-in user. Default null.
* @param array $lessons An array of lesson IDs.
* @param int $course_id Optional. Course ID. Default 0.
*
* @return boolean Returns true if the lesson is not complete otherwise false.
*/
function learndash_is_lesson_notcomplete( $user_id = null, $lessons = array(), $course_id = 0 ) {
if ( ( defined( 'LEARNDASH_COURSE_FUNCTIONS_LEGACY' ) ) && ( true === LEARNDASH_COURSE_FUNCTIONS_LEGACY ) ) {
return learndash_is_lesson_notcomplete_legacy( $user_id, $lessons, $course_id );
}
if ( empty( $user_id ) ) {
$current_user = wp_get_current_user();
$user_id = $current_user->ID;
}
$course_id = absint( $course_id );
if ( ! empty( $lessons ) ) {
foreach ( $lessons as $lesson => $v ) {
if ( empty( $course_id ) ) {
$course_id = learndash_get_course_id( $lesson );
}
if ( ! empty( $course_id ) ) {
$course_progress = learndash_user_get_course_progress( $user_id, $course_id, 'legacy' );
if ( ( isset( $course_progress['lessons'][ $lesson ] ) ) && ( ! empty( $course_progress['lessons'][ $lesson ] ) ) ) {
unset( $lessons[ $lesson ] );
}
}
}
}
if ( empty( $lessons ) ) {
return 0;
} else {
return 1;
}
}
/**
* Checks if a topic is complete.
*
* @since 2.3.1
* @since 3.2.0 Added $course_id
*
* @param int $user_id Optional. User ID. Defaults to the current logged-in user. Default null.
* @param int $topic_id Topic ID.
* @param int $course_id Course ID.
*
* @return boolean Returns true if the topic is completed otherwise false.
*/
function learndash_is_topic_complete( $user_id = null, $topic_id = 0, $course_id = 0 ) {
return ! learndash_is_topic_notcomplete( $user_id, array( $topic_id => 1 ), $course_id );
}
/**
* Checks if a topic is not complete.
*
* @since 2.3.1
* @since 3.2.0 Added $course_id
*
* @param int|null $user_id Optional. User ID. Defaults to the current logged-in user. Default null.
* @param array $topics An array of topic IDs.
* @param int $course_id Course ID.
*
* @return boolean Returns true if the topic is not completed otherwise false.
*/
function learndash_is_topic_notcomplete( $user_id = null, $topics = array(), $course_id = 0 ) {
if ( ( defined( 'LEARNDASH_COURSE_FUNCTIONS_LEGACY' ) ) && ( true === LEARNDASH_COURSE_FUNCTIONS_LEGACY ) ) {
return learndash_is_topic_notcomplete_legacy( $user_id, $topics, $course_id );
}
if ( empty( $user_id ) ) {
$current_user = wp_get_current_user();
$user_id = $current_user->ID;
}
$user_id = absint( $user_id );
$course_id = absint( $course_id );
if ( ! empty( $topics ) ) {
foreach ( $topics as $topic_id => $v ) {
if ( empty( $course_id ) ) {
$course_id = learndash_get_course_id( $topic_id );
}
if ( ! empty( $course_id ) ) {
$course_progress = learndash_user_get_course_progress( $user_id, $course_id, 'legacy' );
$lesson_id = learndash_course_get_single_parent_step( $course_id, $topic_id );
if ( ! empty( $lesson_id ) ) {
if ( ( isset( $course_progress['topics'] ) )
&& ( ! empty( $course_progress['topics'] ) )
&& ( isset( $course_progress['topics'][ $lesson_id ][ $topic_id ] ) )
&& ( ! empty( $course_progress['topics'][ $lesson_id ][ $topic_id ] ) ) ) {
unset( $topics[ $topic_id ] );
}
}
}
}
}
if ( empty( $topics ) ) {
return 0;
} else {
return 1;
}
}
/**
* Outputs the current status of the course.
*
* @since 2.1.0
* @since 2.5.8 Added $return_slug parameter.
*
* @param int $course_id Course ID to get status.
* @param int|null $user_id Optional. User ID. Default null.
* @param boolean $return_slug Optional. If false will return translatable string otherwise the status slug. Default false.
*
* @return string The current status of the course.
*/
function learndash_course_status( $course_id, $user_id = null, $return_slug = false ) {
global $learndash_course_statuses;
if ( ( defined( 'LEARNDASH_COURSE_FUNCTIONS_LEGACY' ) ) && ( true === LEARNDASH_COURSE_FUNCTIONS_LEGACY ) ) {
return learndash_course_status_legacy( $course_id, $user_id, $return_slug );
}
$course_status_slug = '';
$course_id = absint( $course_id );
$user_id = absint( $user_id );
if ( empty( $user_id ) ) {
if ( is_user_logged_in() ) {
$user_id = get_current_user_id();
}
}
if ( ( empty( $course_id ) ) || ( empty( $user_id ) ) ) {
return $course_status_slug;
}
$course_progress = learndash_user_get_course_progress( $user_id, $course_id, 'legacy' );
if ( isset( $course_progress['status'] ) ) {
$course_status_slug = $course_progress['status'];
}
/**
* Sort of a kludge for now. If the course is not complete and the 'total' steps is
* not zero. Call the the mark complete function.
*
* This logic should be within User Course Progression class.
*/
if ( in_array( $course_status_slug, array( 'in_progress', 'not_started' ), true ) ) {
if ( ( $course_progress['total'] > 0 ) && ( $course_progress['completed'] === $course_progress['total'] ) ) {
if ( learndash_process_mark_complete( $user_id, $course_id ) ) {
// If the Mark Complete was success we call to get the progress again.
$course_progress = learndash_user_get_course_progress( $user_id, $course_id, 'legacy' );
if ( isset( $course_progress['status'] ) ) {
$course_status_slug = $course_progress['status'];
}
}
}
}
if ( true === $return_slug ) {
return $course_status_slug;
} else {
if ( isset( $learndash_course_statuses[ $course_status_slug ] ) ) {
/**
* Filters the current status of the course.
*
* @param string $course_status_str The translatable current course status string.
* @param int $course_id Course ID.
* @param int $user_id User ID.
* @param array $course_progress Current course progress.
*/
return apply_filters(
'learndash_course_status',
learndash_course_status_label( $course_status_slug ),
$course_id,
$user_id,
isset( $course_progress ) ? $course_progress : array()
);
}
}
return $course_status_slug;
}
/**
* Gets the course status index from the course status label.
*
* In various places with LD the course status is expressed as a string as in 'Not Started', 'In Progress' or 'Complete'.
* the problem with using this string is it will be translated depending on the locale(). This means comparative logic can
* possible fails.
* The purpose of this function is to help use an internal key to keep track of the course status value.
*
* @global array $learndash_course_statuses An array of course status.
*
* @since 2.3.0
*
* @param string $course_status_label Optional. The current translatable text for course status. Default empty.
*
* @return string The index/key of the course status string if found in the `$learndash_course_statuses` global array.
*/
function learndash_course_status_idx( $course_status_label = '' ) {
global $learndash_course_statuses;
return array_search( $course_status_label, $learndash_course_statuses, true );
}
/**
* Get the Course Status label from slug.
*
* @since 3.4.0
*
* @param string $course_status_slug Course Status slug.
*
* @return string|null.
*/
function learndash_course_status_label( $course_status_slug = '' ) {
global $learndash_course_statuses;
if ( isset( $learndash_course_statuses[ $course_status_slug ] ) ) {
return $learndash_course_statuses[ $course_status_slug ];
}
}
/**
* Checks if the quiz is accessible to the user.
*
* @since 3.4.0
*
* @param int|null $user_id $user_id Optional. The ID of User to check. Defaults to the current logged-in user. Default null.
* @param WP_Post|null $post Optional. The `WP_Post` quiz object. Default null.
* @param boolean $return_incomplete Optional. Whether to return last incomplete step. Default false.
* @param int $course_id Optional. Course ID. Default 0.
*
* @return int|WP_Post|void Returns 1 if the quiz is accessible by user otherwise 0. If the `$return_incomplete`
* parameter is set to true it may return `WP_Post` object for incomplete step.
*/
function learndash_is_quiz_accessable( $user_id = null, $post = null, $return_incomplete = false, $course_id = 0 ) {
// Allow using the legacy function in case of issues with new logic.
if ( ( defined( 'LEARNDASH_IS_QUIZ_ACCESSABLE_LEGACY' ) && ( LEARNDASH_IS_QUIZ_ACCESSABLE_LEGACY === true ) ) ) {
return learndash_is_quiz_accessable_legacy( $user_id, $post, $course_id );
}
if ( empty( $user_id ) ) {
$current_user = wp_get_current_user();
if ( empty( $current_user->ID ) ) {
return 1;
}
$user_id = $current_user->ID;
}
if ( ( empty( $post ) ) || ( ! is_a( $post, 'WP_Post' ) ) ) {
return;
}
if ( empty( $course_id ) ) {
$course_id = learndash_get_course_id( $post );
}
$course_id = absint( $course_id );
// If we have a Quiz but the Quiz is not part of a course then return 1 for valid.
if ( empty( $course_id ) ) {
return 1;
}
$bypass_course_limits_admin_users = learndash_can_user_bypass( $user_id, 'learndash_quiz_accessable', $post, $course_id );
if ( true === $bypass_course_limits_admin_users ) {
return 1;
}
$course_progress_co = learndash_user_get_course_progress( $user_id, $course_id, 'co' );
if ( learndash_is_course_builder_enabled() ) {
$quiz_parent_id = learndash_course_get_single_parent_step( $course_id, $post->ID );
} else {
$quiz_parent_id = learndash_get_setting( $post, 'lesson' );
}
$quiz_parent_id = absint( $quiz_parent_id );
if ( ! empty( $quiz_parent_id ) ) {
$quiz_parent_post = get_post( $quiz_parent_id );
if ( is_a( $quiz_parent_post, 'WP_Post' ) ) {
if ( learndash_get_post_type_slug( 'topic' ) === $quiz_parent_post->post_type ) {
$quiz_parent_topic_post = $quiz_parent_post;
$quiz_parent_lesson_id = learndash_get_setting( $quiz_parent_topic_post, 'lesson' );
$quiz_parent_lesson_post = get_post( $quiz_parent_lesson_id );
$parent_topic_quizzes = learndash_get_lesson_quiz_list( $quiz_parent_topic_post, $user_id, $course_id );
if ( ! empty( $parent_topic_quizzes ) ) {
// loop until we get to the first status == 'notcompleted'.
foreach ( $parent_topic_quizzes as $quiz ) {
if ( $quiz['post']->ID === $post->ID ) {
break;
} elseif ( 'completed' !== $quiz['status'] ) {
if ( true === $return_incomplete ) {
return $quiz['post'];
} else {
return 0;
}
}
}
}
$lesson_topics_progress = learndash_get_course_progress( $user_id, $quiz_parent_topic_post->ID );
if ( ( isset( $lesson_topics_progress['posts'] ) ) && ( ! empty( $lesson_topics_progress['posts'] ) ) ) {
foreach ( $lesson_topics_progress['posts'] as $topic ) {
if ( $topic->ID === $quiz_parent_topic_post->ID ) {
if ( ! empty( $topic->completed ) ) {
return 1;
}
break;
}
if ( empty( $topic->completed ) ) {
if ( true === $return_incomplete ) {
return $topic;
} else {
return 0;
}
break;
}
}
}
if ( 'on' === learndash_get_setting( $quiz_parent_topic_post->ID, 'lesson_video_enabled' ) ) {
if ( ! empty( learndash_get_setting( $quiz_parent_topic_post->ID, 'lesson_video_url' ) ) ) {
if ( 'BEFORE' === learndash_get_setting( $quiz_parent_topic_post->ID, 'lesson_video_shown' ) ) {
if ( ! learndash_video_complete_for_step( $quiz_parent_topic_post->ID, $course_id, $user_id ) ) {
if ( true === $return_incomplete ) {
return $quiz_parent_topic_post;
} else {
return 0;
}
}
}
}
}
$lesson_progress = learndash_get_course_progress( $user_id, $quiz_parent_lesson_post->ID, $course_id );
if ( ( isset( $lesson_progress['posts'] ) ) && ( ! empty( $lesson_progress['posts'] ) ) ) {
foreach ( $lesson_progress['posts'] as $lesson ) {
if ( $lesson->ID === $quiz_parent_lesson_post->ID ) {
break;
}
if ( empty( $lesson->completed ) ) {
if ( true === $return_incomplete ) {
return $lesson;
} else {
return 0;
}
break;
}
}
}
return 1;
} elseif ( learndash_get_post_type_slug( 'lesson' ) === $quiz_parent_post->post_type ) {
$quiz_parent_lesson_post = $quiz_parent_post;
$sibling_completed_steps = 0;
$lesson_topics = learndash_get_topic_list( $quiz_parent_lesson_post->ID );
if ( ! empty( $lesson_topics ) ) {
$lesson_topics_progress = learndash_get_course_progress( $user_id, $lesson_topics[0]->ID );
if ( ( isset( $lesson_topics_progress['posts'] ) ) && ( ! empty( $lesson_topics_progress['posts'] ) ) ) {
foreach ( $lesson_topics_progress['posts'] as $topic ) {
if ( empty( $topic->completed ) ) {
if ( true === $return_incomplete ) {
return $topic;
} else {
return 0;
}
break;
} else {
++$sibling_completed_steps;
}
}
}
}
$quizzes = learndash_get_lesson_quiz_list( $quiz_parent_lesson_post, $user_id, $course_id );
if ( ! empty( $quizzes ) ) {
// loop until we get to the first status == 'notcompleted'.
foreach ( $quizzes as $quiz ) {
if ( $quiz['post']->ID === $post->ID ) {
break;
}
if ( 'completed' !== $quiz['status'] ) {
if ( true === $return_incomplete ) {
return $quiz['post'];
} else {
return 0;
}
} else {
++$sibling_completed_steps;
}
}
}
$lesson_progress = learndash_get_course_progress( $user_id, $quiz_parent_lesson_post->ID );
if ( ( isset( $lesson_progress['posts'] ) ) && ( ! empty( $lesson_progress['posts'] ) ) ) {
foreach ( $lesson_progress['posts'] as $lesson ) {
if ( $lesson->ID === $quiz_parent_lesson_post->ID ) {
if ( ! empty( $lesson->completed ) ) {
return 1;
}
break;
}
if ( empty( $lesson->completed ) ) {
if ( true === $return_incomplete ) {
return $lesson;
} else {
return 0;
}
break;
}
}
}
if ( empty( $sibling_completed_steps ) ) {
if ( 'on' === learndash_get_setting( $quiz_parent_lesson_post->ID, 'lesson_video_enabled' ) ) {
if ( ! empty( learndash_get_setting( $quiz_parent_lesson_post->ID, 'lesson_video_url' ) ) ) {
if ( 'BEFORE' === learndash_get_setting( $quiz_parent_lesson_post->ID, 'lesson_video_shown' ) ) {
if ( ! learndash_video_complete_for_step( $quiz_parent_lesson_post->ID, $course_id, $user_id ) ) {
if ( true === $return_incomplete ) {
return $quiz_parent_lesson_post;
} else {
return 0;
}
}
}
}
}
}
return 1;
}
}
} else {
// First we check if all course lessons are completed.
$lessons = learndash_get_course_lessons_list( $course_id, $user_id, array( 'num' => 0 ) );
if ( ! empty( $lessons ) ) {
foreach ( $lessons as $lesson ) {
if ( 'completed' !== $lesson['status'] ) {
if ( true === $return_incomplete ) {
return $lesson['post'];
} else {
return 0;
}
}
}
}
// Next we check if other global quizzes are completed.
$quizzes = learndash_get_global_quiz_list( $course_id );
if ( ! empty( $quizzes ) ) {
// loop until we get to the first status == 'notcompleted'.
foreach ( $quizzes as $quiz ) {
if ( $quiz->ID === $post->ID ) {
return 1;
} elseif ( ! learndash_is_quiz_complete( $user_id, $quiz->ID, $course_id ) ) {
if ( true === $return_incomplete ) {
return $quiz;
} else {
return 0;
}
}
}
}
}
return 0;
}
/**
* Checks if all quizzes for a course are complete for the user.
*
* @since 3.4.0
*
* @param int|null $user_id Optional. User ID. Default null.
* @param int|null $id Optional. The ID of the resource. Default null.
*
* @return boolean
*/
function learndash_is_all_global_quizzes_complete( $user_id = null, $id = null ) {
$quizzes = learndash_get_global_quiz_list( $id );
$return = true;
if ( ! empty( $quizzes ) ) {
foreach ( $quizzes as $quiz ) {
if ( learndash_is_quiz_notcomplete( $user_id, array( $quiz->ID => 1 ), false, $id ) ) {
$return = false;
}
}
}
return $return;
}
/**
* Check if the user can complete the current step.
*
* This is mostly used before auto-completing a parent step like a quiz
* parent lesson.
*
* @since 3.2.3
* @since 4.0.3 Added `$ignore_lesson_timer` parameter.
*
* @param int $user_id User ID.
* @param int $step_id Course Step ID.
* @param int $course_id Course ID.
* @param bool $ignore_lesson_timer Whether to ignore the lesson timer. Default false. @since 4.0.3.
*
* @return bool True if can complete.
*/
function learndash_can_complete_step( $user_id = 0, $step_id = 0, $course_id = 0, $ignore_lesson_timer = false ) {
$user_id = absint( $user_id );
if ( empty( $user_id ) ) {
$user_id = get_current_user_id();
}
$step_id = absint( $step_id );
$course_id = absint( $course_id );
if ( empty( $course_id ) ) {
$course_id = learndash_get_course_id( $step_id );
}
if ( ( ! empty( $user_id ) ) && ( ! empty( $step_id ) ) && ( ! empty( $course_id ) ) ) {
// If we have ANY previous incomplete steps then we can't complete this step.
if ( ! learndash_lesson_progression_enabled( $course_id ) ) {
$incomplete_child_steps = learndash_user_progression_get_incomplete_child_steps( $user_id, $course_id, $step_id );
if ( empty( ! $incomplete_child_steps ) ) {
return false;
}
} else {
$incomplete_step_id = learndash_user_progress_get_previous_incomplete_step( $user_id, $course_id, $step_id );
if ( ! empty( $incomplete_step_id ) && ( $incomplete_step_id !== $step_id ) ) {
return false;
}
}
if ( in_array( get_post_type( $step_id ), learndash_get_post_type_slug( array( 'lesson', 'topic' ) ), true ) ) {
// Check the lesson/topic timer.
if ( false === $ignore_lesson_timer ) {
$step_timer_time = learndash_forced_lesson_time( $step_id );
if ( ! empty( $step_timer_time ) ) {
$time_cookie_key = learndash_forced_lesson_time_cookie_key( $step_id );
if ( ! empty( $time_cookie_key ) ) {
/**
* Note this is not a 100% check. We are only checking if the cookie
* key exists and is zero. But this cookie could have been set from
* external.
*/
if ( ( ! isset( $_COOKIE[ 'learndash_timer_cookie_' . $time_cookie_key ] ) ) || ( '0' !== $_COOKIE[ 'learndash_timer_cookie_' . $time_cookie_key ] ) ) {
return false;
}
}
}
}
// Next check the Lesson Assignment.
if ( 'on' === learndash_get_setting( $step_id, 'lesson_assignment_upload' ) ) {
$assignments = learndash_get_user_assignments( $step_id, $user_id, $course_id );
if ( empty( $assignments ) ) {
return false;
} else {
foreach ( $assignments as $assignment ) {
if ( ! learndash_is_assignment_approved_by_meta( $assignment->ID ) ) {
return false;
}
}
}
}
// Next check if all child steps are completed.
if ( ! learndash_user_is_course_children_progress_complete( $user_id, $course_id, $step_id ) ) {
return false;
}
}
// Check external steps.
if ( learndash_course_steps_is_external_attendance_required( $step_id ) ) {
return false;
}
return true;
}
return false;
}
/**
* Checks if the resource has any quizzes.
*
* @since 3.4.0
*
* @param int|null $id Optional. The ID of the resource like course, lesson, topic, etc. Default null.
*
* @return boolean Returns true if the resource has quizzes otherwise false.
*/
function learndash_has_global_quizzes( $id = null ) {
$quizzes = learndash_get_global_quiz_list( $id );
return ! empty( $quizzes );
}
/**
* Checks whether the lesson progression is enabled or not.
*
* @since 2.1.0
*
* @param int $course_id Optional. Course ID to check. Default 0.
*
* @return boolean Returns true if the lesson progression is enabled otherwise false.
*/
function learndash_lesson_progression_enabled( $course_id = 0 ) {
$course_id = intval( $course_id );
if ( empty( $course_id ) ) {
$course_id = learndash_get_course_id();
}
if ( ! empty( $course_id ) ) {
$setting = learndash_get_setting( $course_id, 'course_disable_lesson_progression' );
/**
* Filter for Course Progression Enabled.
*
* @since 3.4.0
*
* @param bool $setting true if course progression is enabled.
* @param int $course_id Course ID.
*/
return apply_filters( 'learndash_course_progression_enabled', empty( $setting ), $course_id );
}
return false;
}
/**
* Gets the lesson time for a lesson if it exists.
*
* @global WP_Post $post Global post object.
*
* @since 2.1.0
*
* @param string|int|WP_Post $lesson_topic_post Optional. The `WP_Post` lesson topic post object or ID. Defaults to global post object. Default empty.
*
* @return int|string Returns lesson time if it exists otherwise 0.
*/
function learndash_forced_lesson_time( $lesson_topic_post = '' ) {
if ( empty( $lesson_topic_post ) ) {
global $post;
$lesson_topic_post = $post;
}
if ( ! is_a( $lesson_topic_post, 'WP_Post' ) ) {
$post_id = absint( $lesson_topic_post );
if ( empty( $post_id ) ) {
return 0;
}
$lesson_topic_post = get_post( $post_id );
if ( ( ! $lesson_topic_post ) || ( ! is_a( $lesson_topic_post, 'WP_Post' ) ) ) {
return 0;
}
}
if ( ! in_array( $lesson_topic_post->post_type, array( learndash_get_post_type_slug( 'lesson' ), learndash_get_post_type_slug( 'topic' ) ), true ) ) {
return 0;
}
$meta = get_post_meta( $lesson_topic_post->ID, '_' . $lesson_topic_post->post_type, true );
if ( ! is_array( $meta ) ) {
$meta = array();
}
if ( ! isset( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time_enabled' ] ) ) {
if ( ( isset( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ] ) ) && ( ! empty( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ] ) ) ) {
$meta[ $lesson_topic_post->post_type . '_forced_lesson_time_enabled' ] = 'on';
} else {
$meta[ $lesson_topic_post->post_type . '_forced_lesson_time_enabled' ] = '';
}
}
if ( 'on' === $meta[ $lesson_topic_post->post_type . '_forced_lesson_time_enabled' ] ) {
if ( ( isset( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ] ) ) && ( ! empty( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ] ) ) ) {
return $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ];
}
}
return 0;
}
/**
* Gets the lesson time cookie key for lesson/topic.
*
* @global WP_Post $post Global post object.
*
* @since 3.0.0
*
* @param string|int|WP_Post $lesson_topic_post Optional. The `WP_Post` lesson topic post object or ID. Defaults to global post object.
*
* @return string The cookie key value or empty string.
*/
function learndash_forced_lesson_time_cookie_key( $lesson_topic_post = '' ) {
if ( empty( $lesson_topic_post ) ) {
global $post;
$lesson_topic_post = $post;
}
if ( ! is_a( $lesson_topic_post, 'WP_Post' ) ) {
$post_id = absint( $lesson_topic_post );
if ( empty( $post_id ) ) {
return 0;
}
$lesson_topic_post = get_post( $post_id );
if ( ( ! $lesson_topic_post ) || ( ! is_a( $lesson_topic_post, 'WP_Post' ) ) ) {
return 0;
}
}
if ( ! in_array( $lesson_topic_post->post_type, array( learndash_get_post_type_slug( 'lesson' ), learndash_get_post_type_slug( 'topic' ) ), true ) ) {
return 0;
}
$meta = get_post_meta( $lesson_topic_post->ID, '_' . $lesson_topic_post->post_type, true );
if ( ! is_array( $meta ) ) {
$meta = array();
}
if ( ! isset( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time_enabled' ] ) ) {
if ( ( isset( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ] ) ) && ( ! empty( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ] ) ) ) {
$meta[ $lesson_topic_post->post_type . '_forced_lesson_time_enabled' ] = 'on';
} else {
$meta[ $lesson_topic_post->post_type . '_forced_lesson_time_enabled' ] = '';
}
}
if ( 'on' === $meta[ $lesson_topic_post->post_type . '_forced_lesson_time_enabled' ] ) {
if ( ( isset( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ] ) ) && ( ! empty( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time' ] ) ) ) {
$cookie_key = get_current_user_id() . '_' . learndash_get_course_id( $lesson_topic_post ) . '_' . $lesson_topic_post->ID;
if ( ( isset( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time_cookie_key' ] ) ) && ( ! empty( $meta[ $lesson_topic_post->post_type . '_forced_lesson_time_cookie_key' ] ) ) ) {
$cookie_key .= '_' . $meta[ $lesson_topic_post->post_type . '_forced_lesson_time_cookie_key' ];
}
return $cookie_key;
}
}
return '';
}
/**
* Checks if a course is completed for a user.
*
* @since 2.1.0
*
* @param int $user_id User ID.
* @param int $course_id Course ID.
*
* @return boolean Returns true if the course is completed otherwise false.
*/
function learndash_course_completed( $user_id, $course_id ) {
if ( learndash_course_status( $course_id, $user_id ) == esc_html__( 'Completed', 'learndash' ) ) {
return true;
} else {
return false;
}
}
/**
* Adds the course completion date to user meta.
*
* @since 2.1.0
*
* @param array $data An array of course completion data.
*/
function learndash_course_completed_store_time( $data ) {
$user_id = $data['user']->ID;
$course_id = $data['course']->ID;
$meta_key = 'course_completed_' . $course_id;
$meta_value = time();
$course_completed = get_user_meta( $user_id, $meta_key );
if ( empty( $course_completed ) ) {
update_user_meta( $user_id, $meta_key, $meta_value );
}
}
add_action( 'learndash_before_course_completed', 'learndash_course_completed_store_time', 10, 1 );
/**
* Deletes the course progress for a user.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @since 2.1.0
*
* @param int $course_id Course ID.
* @param int $user_id User ID.
*/
function learndash_delete_course_progress( $course_id, $user_id ) {
global $wpdb;
$usermeta = get_user_meta( $user_id, '_sfwd-course_progress', true );
if ( isset( $usermeta[ $course_id ] ) ) {
unset( $usermeta[ $course_id ] );
update_user_meta( $user_id, '_sfwd-course_progress', $usermeta );
}
delete_user_meta( $user_id, 'course_completed_' . $course_id );
// The reason we don't use the methods above is we want to ensure all old data is removed
// from the quiz attempt history not just for quizzes currently associated with the course.
$quizzes = array();
$usermeta_quizzes = get_user_meta( $user_id, '_sfwd-quizzes', true );
if ( ! is_array( $usermeta_quizzes ) ) {
$usermeta_quizzes = array();
}
if ( ! empty( $usermeta_quizzes ) ) {
foreach ( $usermeta_quizzes as $quiz_item ) {
if ( ( isset( $quiz_item['course'] ) ) && ( intval( $course_id ) == intval( $quiz_item['course'] ) ) ) {
if ( isset( $quiz_item['quiz'] ) ) {
$quiz_id = intval( $quiz_item['quiz'] );
$quizzes[ $quiz_id ] = $quiz_id;
}
}
}
}
if ( ! empty( $quizzes ) ) {
foreach ( $quizzes as $quiz_id ) {
learndash_delete_quiz_progress( $user_id, $quiz_id );
}
}
}
/**
* Deletes the quiz progress for a user.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @since 2.1.0
*
* @param int $user_id User ID.
* @param int $quiz_id Quiz ID.
*/
function learndash_delete_quiz_progress( $user_id, $quiz_id ) {
global $wpdb;
// Clear User Meta.
$usermeta = get_user_meta( $user_id, '_sfwd-quizzes', true );
if ( ! empty( $usermeta ) && is_array( $usermeta ) ) {
$usermeta_new = array();
foreach ( $usermeta as $key => $quizmeta ) {
if ( $quizmeta['quiz'] != $quiz_id ) {
$usermeta_new[] = $quizmeta;
}
}
update_user_meta( $user_id, '_sfwd-quizzes', $usermeta_new );
}
// ProQuiz Data.
$pro_quiz_id = learndash_get_setting( $quiz_id, 'quiz_pro' );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$ref_ids = $wpdb->get_col(
$wpdb->prepare( 'SELECT statistic_ref_id FROM ' . esc_sql( LDLMS_DB::get_table_name( 'quiz_statistic_ref' ) ) . ' WHERE user_id = %d AND quiz_id = %d ', $user_id, $pro_quiz_id )
);
if ( ! empty( $ref_ids[0] ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete(
LDLMS_DB::get_table_name( 'quiz_statistic_ref' ),
array(
'user_id' => $user_id,
'quiz_id' => $pro_quiz_id,
)
);
$ref_ids = array_map( 'absint', $ref_ids );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
// phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared -- IN clause
$wpdb->prepare( 'DELETE FROM ' . esc_sql( LDLMS_DB::get_table_name( 'quiz_statistic' ) ) . ' WHERE statistic_ref_id IN (' . LDLMS_DB::escape_IN_clause_placeholders( $ref_ids ) . ')', LDLMS_DB::escape_IN_clause_values( $ref_ids ) )
);
}
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete(
LDLMS_DB::get_table_name( 'quiz_toplist' ),
array(
'user_id' => $user_id,
'quiz_id' => $pro_quiz_id,
)
);
}
/**
* Removes the user quiz statistics by the reference ID.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @since 2.5.0
*
* @param int $ref_id Optional. Quiz statistic reference ID. Default 0.
*/
function learndash_quiz_remove_user_statistics_by_ref( $ref_id = 0 ) {
global $wpdb;
if ( ! empty( $ref_id ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->delete( LDLMS_DB::get_table_name( 'quiz_statistic_ref' ), array( 'statistic_ref_id' => $ref_id ) );
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
$wpdb->query(
$wpdb->prepare( 'DELETE FROM ' . esc_sql( LDLMS_DB::get_table_name( 'quiz_statistic' ) ) . ' WHERE statistic_ref_id = %d', $ref_id )
);
}
}
/**
* Removes the quiz user toplist.
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @since 3.1.0
*
* @param int $user_id Optional. User ID. Default 0.
* @param int $quiz_time Optional. Quiz time. Default 0.
* @param int $pro_quizid Optional. Pro quiz ID. Default 0.
*
* @return void|int|false The number of rows updated, or false on error.
*/
function learndash_quiz_remove_user_toplist( $user_id = 0, $quiz_time = 0, $pro_quizid = 0 ) {
global $wpdb;
if ( ( ! empty( $user_id ) ) && ( ! empty( $quiz_time ) ) && ( ! empty( $pro_quizid ) ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
return $wpdb->delete(
LDLMS_DB::get_table_name( 'quiz_toplist' ),
array(
'user_id' => $user_id,
'date' => $quiz_time,
'quiz_id' => $pro_quizid,
),
array( '%d', '%d', '%d' )
);
}
}
/**
* Marks a course step incomplete for a course.
*
* Used to set a course step ( lesson or topic only ) back to not complete status.
*
* @global WP_Post $post Global post object.
*
* @since 2.5.0
*
* @param int $user_id Optional. User ID. Default 0.
* @param int $course_id Optional. Course ID. Default 0.
* @param int $step_id Optional. Step ID. Default 0.
* @param boolean $step_complete Optional. Unused. Default false.
*
* @return int|boolean Returns true if the update is successful otherwise false or meta ID
* if the meta does not exist.
*/
function learndash_process_mark_incomplete( $user_id = 0, $course_id = 0, $step_id = 0, $step_complete = false ) {
$user_id = absint( $user_id );
if ( empty( $user_id ) ) {
return;
}
$course_id = absint( $course_id );
if ( empty( $course_id ) ) {
return;
}
$step_id = absint( $step_id );
if ( empty( $step_id ) ) {
global $post;
if ( ( isset( $post ) ) && ( $post instanceof WP_Post ) && ( ( in_array( $post->post_type, array( 'sfwd-lessons', 'sfwd-topic', 'sfwd-quiz' ), true ) ) ) ) {
$step_id = $post->ID;
} else {
return;
}
}
$subtracted_completed_steps = 0;
$course_step_parents = learndash_course_get_all_parent_step_ids( $course_id, $step_id );
$user_course_progress = get_user_meta( $user_id, '_sfwd-course_progress', true );
if ( ! isset( $user_course_progress[ $course_id ] ) ) {
return;
}
$step_post_type = get_post_type( $step_id );
if ( 'sfwd-quiz' === $step_post_type ) {
if ( ! empty( $course_step_parents ) ) {
if ( ( 2 == count( $course_step_parents ) ) && ( 'sfwd-lessons' == get_post_type( $course_step_parents[0] ) ) && ( 'sfwd-topic' == get_post_type( $course_step_parents[1] ) ) ) {
$lesson_id = $course_step_parents[0];
$topic_id = $course_step_parents[1];
if ( ( isset( $user_course_progress[ $course_id ]['topics'][ $lesson_id ][ $topic_id ] ) ) && ( true == $user_course_progress[ $course_id ]['topics'][ $lesson_id ][ $topic_id ] ) ) {
$user_course_progress[ $course_id ]['topics'][ $lesson_id ][ $topic_id ] = 0;
$user_course_progress[ $course_id ]['completed'] -= 1;
/**
* Fires on marking a course incomplete.
*
* @param int $user_id User ID.
* @param int $course_id Course ID.
* @param int $topic_id Topic ID.
*/
do_action( 'learndash_mark_incomplete_process', $user_id, $course_id, $topic_id );
$topic_args = array(
'course_id' => $course_id,
'user_id' => $user_id,
'post_id' => $topic_id,
'activity_type' => 'topic',
);
$topic_activity = learndash_get_user_activity( $topic_args );
if ( $topic_activity ) {
$topic_activity = (array) $topic_activity;
$topic_activity['activity_status'] = false;
$topic_activity['activity_completed'] = false;
learndash_update_user_activity( $topic_activity );
}
}
if ( ( isset( $user_course_progress[ $course_id ]['lessons'][ $lesson_id ] ) ) && ( true == $user_course_progress[ $course_id ]['lessons'][ $lesson_id ] ) ) {
$user_course_progress[ $course_id ]['lessons'][ $lesson_id ] = 0;
$user_course_progress[ $course_id ]['completed'] -= 1;
/** This filter is documented in includes/course/ld-course-progress.php */
do_action( 'learndash_mark_incomplete_process', $user_id, $course_id, $lesson_id );
$lesson_args = array(
'course_id' => $course_id,
'user_id' => $user_id,
'post_id' => $lesson_id,
'activity_type' => 'lesson',
);
$lesson_activity = learndash_get_user_activity( $lesson_args );
if ( $lesson_args ) {
$lesson_args = (array) $lesson_args;
$lesson_args['activity_status'] = false;
$lesson_args['activity_completed'] = false;
learndash_update_user_activity( $lesson_args );
}
}
} elseif ( ( 1 == count( $course_step_parents ) ) && ( 'sfwd-lessons' == get_post_type( $course_step_parents[0] ) ) ) {
$lesson_id = $course_step_parents[0];
if ( ( isset( $user_course_progress[ $course_id ]['lessons'][ $lesson_id ] ) ) && ( true == $user_course_progress[ $course_id ]['lessons'][ $lesson_id ] ) ) {
$user_course_progress[ $course_id ]['lessons'][ $lesson_id ] = 0;
$user_course_progress[ $course_id ]['completed'] -= 1;
/** This filter is documented in includes/course/ld-course-progress.php */
do_action( 'learndash_mark_incomplete_process', $user_id, $course_id, $lesson_id );
$lesson_args = array(
'course_id' => $course_id,
'user_id' => $user_id,
'post_id' => $lesson_id,
'activity_type' => 'lesson',
);
$lesson_activity = learndash_get_user_activity( $lesson_args );
if ( $lesson_args ) {
$lesson_args = (array) $lesson_args;
$lesson_args['activity_status'] = false;
$lesson_args['activity_completed'] = false;
learndash_update_user_activity( $lesson_args );
}
}
}
}
} elseif ( 'sfwd-topic' === $step_post_type ) {
$step_parent_id = learndash_course_get_single_parent_step( $course_id, $step_id );
if ( ! empty( $step_parent_id ) ) {
if ( ( isset( $user_course_progress[ $course_id ]['topics'][ $step_parent_id ][ $step_id ] ) ) && ( true == $user_course_progress[ $course_id ]['topics'][ $step_parent_id ][ $step_id ] ) ) {
$user_course_progress[ $course_id ]['topics'][ $step_parent_id ][ $step_id ] = 0;
$user_course_progress[ $course_id ]['completed'] -= 1;
/** This filter is documented in includes/course/ld-course-progress.php */
do_action( 'learndash_mark_incomplete_process', $user_id, $course_id, $step_id );
$topic_args = array(
'course_id' => $course_id,
'user_id' => $user_id,
'post_id' => $step_id,
'activity_type' => 'topic',
);
$topic_activity = learndash_get_user_activity( $topic_args );
if ( $topic_activity ) {
$topic_activity = (array) $topic_activity;
$topic_activity['activity_status'] = false;
$topic_activity['activity_completed'] = false;
learndash_update_user_activity( $topic_activity );
}
}
if ( ( isset( $user_course_progress[ $course_id ]['lessons'][ $step_parent_id ] ) ) && ( true == $user_course_progress[ $course_id ]['lessons'][ $step_parent_id ] ) ) {
$user_course_progress[ $course_id ]['lessons'][ $step_parent_id ] = 0;
$user_course_progress[ $course_id ]['completed'] -= 1;
/** This filter is documented in includes/course/ld-course-progress.php */
do_action( 'learndash_mark_incomplete_process', $user_id, $course_id, $step_parent_id );
$lesson_args = array(
'course_id' => $course_id,
'user_id' => $user_id,
'post_id' => $step_parent_id,
'activity_type' => 'lesson',
);
$lesson_activity = learndash_get_user_activity( $lesson_args );
if ( $lesson_args ) {
$lesson_args = (array) $lesson_args;
$lesson_args['activity_status'] = false;
$lesson_args['activity_completed'] = false;
learndash_update_user_activity( $lesson_args );
}
}
}
} elseif ( 'sfwd-lessons' === $step_post_type ) {
if ( ( isset( $user_course_progress[ $course_id ]['lessons'][ $step_id ] ) ) && ( true == $user_course_progress[ $course_id ]['lessons'][ $step_id ] ) ) {
$user_course_progress[ $course_id ]['lessons'][ $step_id ] = 0;
$user_course_progress[ $course_id ]['completed'] -= 1;
/** This filter is documented in includes/course/ld-course-progress.php */
do_action( 'learndash_mark_incomplete_process', $user_id, $course_id, $step_id );
$lesson_args = array(
'course_id' => $course_id,
'user_id' => $user_id,
'post_id' => $step_id,
'activity_type' => 'lesson',
);
$lesson_activity = learndash_get_user_activity( $lesson_args );
if ( $lesson_args ) {
$lesson_args = (array) $lesson_args;
$lesson_args['activity_status'] = false;
$lesson_args['activity_completed'] = false;
learndash_update_user_activity( $lesson_args );
}
}
}
if ( ! isset( $user_course_progress[ $course_id ] ) ) {
return;
}
if ( isset( $user_course_progress[ $course_id ]['completed'] ) ) {
$user_course_progress[ $course_id ]['completed'] = absint( $user_course_progress[ $course_id ]['completed'] );
} else {
$user_course_progress[ $course_id ]['completed'] = 0;
}
if ( isset( $user_course_progress[ $course_id ]['total'] ) ) {
$user_course_progress[ $course_id ]['total'] = absint( $user_course_progress[ $course_id ]['total'] );
} else {
$user_course_progress[ $course_id ]['total'] = 0;
}
if ( $user_course_progress[ $course_id ]['completed'] !== $user_course_progress[ $course_id ]['total'] ) {
delete_user_meta( $user_id, 'course_completed_' . $course_id );
/** This filter is documented in includes/course/ld-course-progress.php */
do_action( 'learndash_mark_incomplete_process', $user_id, $course_id, $course_id );
$course_args = array(
'course_id' => $course_id,
'user_id' => $user_id,
'post_id' => $course_id,
'activity_type' => 'course',
);
$course_activity = learndash_get_user_activity( $course_args );
if ( $course_args ) {
$course_args = (array) $course_args;
$course_args['activity_status'] = false;
$course_args['activity_completed'] = false;
learndash_update_user_activity( $course_args );
}
}
return update_user_meta( $user_id, '_sfwd-course_progress', $user_course_progress );
}
/**
* Gets the quiz attempt meta for a given user.
*
* @since 2.3.0
*
* @param int $user_id Optional. User ID. Default 0.
* @param array $args Optional. An array of items to match. Default empty array.
*
* @return array An array of user quiz attempt meta.
*/
function learndash_get_user_quiz_attempt( $user_id = 0, $args = array() ) {
if ( ( ! empty( $user_id ) ) && ( ! empty( $args ) ) ) {
$user_quizzes = get_user_meta( $user_id, '_sfwd-quizzes', true );
if ( ! empty( $user_quizzes ) ) {
foreach ( $user_quizzes as $idx => $user_quiz ) {
foreach ( $args as $arg_key => $arg_val ) {
if ( ( ! isset( $user_quiz[ $arg_key ] ) ) || ( $user_quiz[ $arg_key ] != $arg_val ) ) {
unset( $user_quizzes[ $idx ] );
}
}
}
}
return $user_quizzes;
}
return array();
}
/**
* Removes the quiz attempt meta for a given user.
*
* @since 2.5.0
*
* @param int $user_id Optional. User ID. Default 0.
* @param array $args Optional. An array of items to match. Default empty array.
*
* @return array An array of updated quiz attempt meta.
*/
function learndash_remove_user_quiz_attempt( $user_id = 0, $args = array() ) {
if ( ( ! empty( $user_id ) ) && ( ! empty( $args ) ) ) {
$changes = false;
$user_quizzes = get_user_meta( $user_id, '_sfwd-quizzes', true );
if ( ! empty( $user_quizzes ) ) {
$changed_user_quizzes = array();
foreach ( $user_quizzes as $idx => $user_quiz ) {
$match_found = true;
foreach ( $args as $arg_key => $arg_val ) {
if ( ( ! isset( $user_quiz[ $arg_key ] ) ) || ( $user_quiz[ $arg_key ] != $arg_val ) ) {
$match_found = false;
break;
}
}
if ( true === $match_found ) {
/**
* Fires before user single quiz attempt has been removed.
*
* @since 3.2.3
*
* @param int $user_id User ID for the Quiz.
* @param array $user_quiz User meta quiz item to be deleted.
*/
do_action( 'learndash_user_quiz_delete_single_before', $user_id, $user_quiz );
if ( ( isset( $user_quiz['time'] ) ) && ( ! empty( $user_quiz['time'] ) ) ) {
if ( ( isset( $user_quiz['pro_quizid'] ) ) && ( ! empty( $user_quiz['pro_quizid'] ) ) ) {
learndash_quiz_remove_user_toplist( $user_id, $user_quiz['time'], $user_quiz['pro_quizid'] );
}
}
// If we have a statistics reference we need to remove this set of records.
if ( ( isset( $user_quiz['statistic_ref_id'] ) ) && ( ! empty( $user_quiz['statistic_ref_id'] ) ) ) {
learndash_quiz_remove_user_statistics_by_ref( $user_quiz['statistic_ref_id'] );
}
if ( ( ! isset( $user_quiz['course'] ) ) || ( empty( $user_quiz['course'] ) ) ) {
$user_quiz['course'] = learndash_get_course_id( $user_quiz['quiz'] );
}
// If this quiz has graded items they all need to be moved to trash or deleted.
if ( ( isset( $user_quiz['graded'] ) ) && ( ! empty( $user_quiz['graded'] ) ) ) {
foreach ( $user_quiz['graded'] as $question_id => $graded_set ) {
if ( ( isset( $graded_set['post_id'] ) ) && ( ! empty( $graded_set['post_id'] ) ) ) {
wp_delete_post( $graded_set['post_id'], true );
}
}
}
// Remove the user activity record.
$quiz_args = array(
'course_id' => isset( $user_quiz['course'] ) ? absint( $user_quiz['course'] ) : 0,
'user_id' => $user_id,
'post_id' => $user_quiz['quiz'],
'activity_type' => 'quiz',
'activity_completed' => isset( $user_quiz['completed'] ) ? absint( $user_quiz['completed'] ) : 0,
);
$quiz_activity = learndash_get_user_activity( $quiz_args );
if ( ! empty( $quiz_activity ) ) {
learndash_delete_user_activity( $quiz_activity->activity_id );
}
$changed_user_quizzes[] = $user_quiz;
unset( $user_quizzes[ $idx ] );
$changes = true;
/**
* Fires after user single quiz attempt has been removed.
*
* @since 3.2.3
*
* @param int $user_id User ID for the Quiz.
* @param array $user_quiz User meta quiz item to be deleted.
*/
do_action( 'learndash_user_quiz_delete_single_after', $user_id, $user_quiz );
}
}
if ( true === $changes ) {
// If not empty then we reset the keys.
if ( ! empty( $user_quizzes ) ) {
$user_quizzes = array_values( $user_quizzes );
}
update_user_meta( $user_id, '_sfwd-quizzes', $user_quizzes );
if ( ! empty( $changed_user_quizzes ) ) {
/**
* Fires after user all quiz attempts have been removed.
*
* @since 3.2.3
*
* @param int $user_id User ID for the Quiz.
* @param array $changed_user_quizzes Array of all quiz items deleted.
*/
do_action( 'learndash_user_quiz_delete_all_after', $user_id, $changed_user_quizzes );
foreach ( $changed_user_quizzes as $user_quiz ) {
if ( ! learndash_is_quiz_complete( $user_id, $user_quiz['quiz'], $user_quiz['course'] ) ) {
learndash_process_mark_incomplete( $user_id, $user_quiz['course'], $user_quiz['quiz'], false );
}
/**
* Legacy: Call the `learndash_process_mark_complete` function after the quiz entry is removed.
*
* The call to this function was removed as of LD 3.5.1 because it should not be needed. But
* retaining for legacy purposes.
*
* @since 3.5.1
*
* @param bool $call_function Default is false.
* @param array $user_quiz User meta quiz item to be deleted.
* @param int $user_id User ID for the Quiz.
*/
if ( true === apply_filters( 'learndash_quiz_call_mark_complete_after_remove', false, $user_quiz, $user_id ) ) {
learndash_process_mark_complete( $user_id, $user_quiz['quiz'], false, $user_quiz['course'] );
}
}
}
}
}
return $user_quizzes;
}
return array();
}
/**
* Outputs HTML output to mark a step incomplete.
*
* Must meet requirements of course to mark incomplete.
*
* @since 3.1.4
*
* @param WP_Post $post The `WP_Post` for lesson, topic.
* @param array $atts Optional. An array of attributes for mark incomplete output. Default empty array.
*
* @return string The HTML output to mark course incomplete.
*/
function learndash_show_mark_incomplete( $post, $atts = array() ) {
$course_mark_incomplete_enabled = LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Courses_Management_Display', 'course_mark_incomplete_enabled', false );
if ( empty( $course_mark_incomplete_enabled ) ) {
return '';
}
if ( ! is_user_logged_in() ) {
return '';
}
$user_id = get_current_user_id();
$course_id = learndash_get_course_id( $post->ID );
/**
* Filters whether to show mark course incomplete form.
*
* @param boolean $show_form Whether to show mark incomplete form.
* @param int $course_id Course ID.
* @param WP_Post $post `WP_Post` object being displayed.
* @param array $atts An array of attributes to mark a course incomplete.
*/
if ( ! apply_filters( 'learndash_show_mark_incomplete_form', true, $course_id, $post, $atts ) ) {
return '';
}
/**
* Filters attributes to mark a course incomplete.
*
* @param array $atts An array of attributes to mark a course incomplete.
* @param WP_Post $post `WP_Post` object being displayed.
*/
$atts = apply_filters( 'learndash_mark_incomplete_form_atts', $atts, $post );
if ( isset( $atts['form']['id'] ) ) {
$form_id = ' id="' . esc_attr( $atts['form']['id'] ) . '" ';
} else {
$form_id = '';
}
if ( isset( $atts['form']['class'] ) ) {
$form_class = ' class="sfwd-mark-incomplete sfwd-mark-complete ' . esc_attr( $atts['form']['class'] ) . '" ';
} else {
$form_class = ' class="sfwd-mark-incomplete sfwd-mark-complete" ';
}
if ( isset( $atts['button']['id'] ) ) {
$button_id = ' id="' . esc_attr( $atts['button']['id'] ) . '" ';
} else {
$button_id = '';
}
$button_disabled = '';
if ( isset( $atts['button']['class'] ) ) {
$button_class = ' class="learndash_mark_incomplete_button learndash_mark_complete_button ' . esc_attr( $atts['button']['class'] ) . '" ';
} else {
$button_class = ' class="learndash_mark_incomplete_button learndash_mark_complete_button" ';
}
$button_label = LearnDash_Custom_Label::get_label( 'button_mark_incomplete' );
if ( empty( $button_label ) ) {
$button_label = esc_html__( 'Mark Incomplete', 'learndash' );
}
$form_fields = '<input type="hidden" value="' . $post->ID . '" name="post" />
<input type="hidden" value="' . learndash_get_course_id( $post->ID ) . '" name="course_id" />
<input type="hidden" value="' . wp_create_nonce( 'sfwd_mark_incomplete_' . $user_id . '_' . $post->ID ) . '" name="sfwd_mark_incomplete" />
<input type="submit" ' . $button_id . ' value="' . esc_attr( $button_label ) . '" ' . $button_class . ' ' . $button_disabled . '/>';
/** This filter is documented in includes/course/ld-course-progress.php */
$form_fields = apply_filters( 'learndash_mark_complete_form_fields', $form_fields, $post );
$return = '<form ' . $form_id . ' ' . $form_class . ' method="post" action="">' . $form_fields . '</form>';
return $return;
}
/**
* Processes the request to mark a course or step incomplete.
*
* @since 3.1.4
*
* @global WP_Post $post Global post object.
*/
function learndash_mark_incomplete_process() {
if ( ( isset( $_POST['sfwd_mark_incomplete'] ) ) && ( ! empty( $_POST['sfwd_mark_incomplete'] ) ) && ( isset( $_POST['post'] ) ) && ( ! empty( $_POST['post'] ) ) ) {
$post_id = intval( $_POST['post'] );
if ( isset( $_POST['course_id'] ) ) {
$course_id = intval( $_POST['course_id'] );
} else {
$course_id = learndash_get_course_id( $post_id );
}
if ( isset( $_POST['userid'] ) ) {
$user_id = intval( $_POST['userid'] );
} else {
if ( ! is_user_logged_in() ) {
return;
}
$user_id = get_current_user_id();
}
/**
* Verify the form is valid
*
* @since 3.1.4
*/
if ( ! wp_verify_nonce( $_POST['sfwd_mark_incomplete'], 'sfwd_mark_incomplete_' . $user_id . '_' . $post_id ) ) {
return;
}
return learndash_process_mark_incomplete( $user_id, $course_id, $post_id, false );
}
return false;
}
add_action( 'wp', 'learndash_mark_incomplete_process' );
/**
* Returns the page url a user is redirected to after completing a step.
*
* @since 4.11.0
*
* @param int $step_id Step post ID.
* @param int $course_id Course ID.
* @param int $user_id User ID.
*
* @return string
*/
function learndash_course_get_step_completion_url( int $step_id, int $course_id, int $user_id ): string {
$redirect_url = '';
$course_completion_transient_key = 'learndash_course_completed_' . $course_id . '_' . $user_id;
if ( false !== get_transient( $course_completion_transient_key ) ) {
/**
* Case 1:
*
* If we have the course completion transient, we should redirect to the completion page.
*/
delete_transient( $course_completion_transient_key ); // Delete, as we need to do it once.
$redirect_url = learndash_course_get_completion_url( $course_id );
} else {
$parent_step_id = learndash_course_get_single_parent_step( $course_id, $step_id );
$parent_step = get_post( $parent_step_id );
$parent_step_is_completed = learndash_user_progress_is_step_complete( $user_id, $course_id, $parent_step_id );
// If the parent step is completed, evaluate the grandparent step.
// It happens when we have a quiz inside a topic. The topic (parent step) may not have any special requirements,
// but the lesson (grandparent step) may have.
if ( $parent_step_is_completed ) {
$parent_step_id = learndash_course_get_single_parent_step( $course_id, $parent_step_id );
$parent_step = get_post( $parent_step_id );
}
// Case 2:
// If the parent step is not completed and requires a video, an assignment, or wait for a timer to be completed, and all the child steps are completed,
// redirect back to the parent step.
if (
$parent_step instanceof WP_Post
&& ! learndash_user_progress_is_step_complete( $user_id, $course_id, $parent_step_id )
&& (
learndash_course_steps_requires_watching_video_after_sub_steps( $parent_step_id )
|| learndash_lesson_hasassignments( $parent_step ) // cspell: disable-line .
|| learndash_forced_lesson_time( $parent_step_id )
|| learndash_course_steps_is_external( $parent_step_id )
)
&& empty( learndash_user_progression_get_incomplete_child_steps( $user_id, $course_id, $parent_step_id ) )
) {
// Redirect back to the parent step.
$redirect_url = learndash_get_step_permalink( $parent_step_id, $course_id );
} else {
$step = get_post( $step_id );
if (
$step instanceof WP_Post
&& (
learndash_course_steps_requires_watching_video_after_sub_steps( $step_id )
|| learndash_lesson_hasassignments( $step ) // cspell: disable-line .
|| learndash_forced_lesson_time( $step_id )
|| learndash_course_steps_is_external( $step_id )
)
) {
/**
* Case 3:
*
* If the current step:
* - requires watching a video after sub steps, or
* - has assignments, or
* - has a timer, or
* - is external,
* and all child steps are completed, redirect to the next incomplete step.
*/
// Get the next incomplete step (an incomplete step after the current step).
$next_step_id = learndash_user_progress_get_next_incomplete_step( $user_id, $course_id, $step_id );
if ( $next_step_id > 0 ) {
// Redirect to the next incomplete step.
$redirect_url = learndash_get_step_permalink( $next_step_id, $course_id );
}
}
}
}
// If there is no special case, redirect to the next step page or a course page.
if ( empty( $redirect_url ) ) {
// Default redirect: the next step page or a course page.
$redirect_url = learndash_next_post_link(
Cast::to_string( get_permalink( $course_id ) ),
true,
get_post( $step_id )
);
}
$redirect_url = Cast::to_string( $redirect_url );
/**
* Filters URL to redirect to after marking a step complete.
*
* @deprecated 4.11.0 Use {@see 'learndash_course_step_completion_url'} instead.
*
* @param string $redirect_url Next step redirect URL. Can be empty if post was not found for some reason.
* @param int $step_id Course step post ID.
*
* @return string Next step redirect URL.
*/
$redirect_url = apply_filters_deprecated(
'learndash_completion_redirect',
[ $redirect_url, $step_id ],
'4.11.0',
'learndash_course_step_completion_url'
);
/**
* Filters URL to redirect to after marking a step complete.
*
* @since 4.11.0
*
* @param string $redirect_url Next step redirect URL. Can be empty if post was not found for some reason.
* @param int $step_id Course step post ID.
* @param int $course_id Course ID.
*
* @return string Redirect URL.
*/
return apply_filters( 'learndash_course_step_completion_url', $redirect_url, $step_id, $course_id );
}