includes/functions/llms.functions.access.php
<?php
/**
* Functions used for managing page / post access
*
* @package LifterLMS/Functions
*
* @since 1.0.0
* @version 6.5.0
*/
defined( 'ABSPATH' ) || exit;
/**
* Determine if content should be restricted.
*
* Called during "template_include" to determine if redirects
* or template overrides are in order.
*
* @since 1.0.0
* @since 3.16.11 Unknown.
* @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.
*
* @param int $post_id WordPress Post ID of the content.
* @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.
* @return array Restriction check result data.
*/
function llms_page_restricted( $post_id, $user_id = null ) {
$results = array(
'content_id' => $post_id,
'is_restricted' => false,
'reason' => 'accessible',
'restriction_id' => 0,
);
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
$student = false;
if ( $user_id ) {
$student = new LLMS_Student( $user_id );
}
$post_type = get_post_type( $post_id );
/**
* Do checks to determine if the content should be restricted.
*/
$sitewide_membership_id = llms_is_post_restricted_by_sitewide_membership( $post_id, $user_id );
$membership_id = llms_is_post_restricted_by_membership( $post_id, $user_id );
if ( is_home() && $sitewide_membership_id ) {
$restriction_id = $sitewide_membership_id;
$reason = 'sitewide_membership';
// if it's a search page and the site isn't restricted to a membership bypass restrictions.
} elseif ( ( is_search() ) && ! get_option( 'lifterlms_membership_required', '' ) ) {
return apply_filters( 'llms_page_restricted', $results, $post_id );
} elseif ( is_singular() && $sitewide_membership_id ) {
$restriction_id = $sitewide_membership_id;
$reason = 'sitewide_membership';
} elseif ( is_singular() && $membership_id ) {
$restriction_id = $membership_id;
$reason = 'membership';
} elseif ( is_singular() && 'lesson' === $post_type ) {
$lesson = new LLMS_Lesson( $post_id );
// if lesson is free, return accessible results and skip the rest of this function.
if ( $lesson->is_free() ) {
return $results;
} else {
$restriction_id = $lesson->get( 'parent_course' );
$reason = 'enrollment_lesson';
}
} elseif ( is_singular() && 'course' === $post_type ) {
$restriction_id = $post_id;
$reason = 'enrollment_course';
} elseif ( is_singular() && 'llms_membership' === $post_type ) {
$restriction_id = $post_id;
$reason = 'enrollment_membership';
} else {
/**
* Allow filtering of results before checking if the student has access.
*
* @since Unknown.
*
* @param array $results Restriction check result data.
* @param int $post_id WordPress Post ID of the content.
*/
$results = apply_filters( 'llms_page_restricted_before_check_access', $results, $post_id );
extract( $results ); // phpcs:ignore
}
/**
* Content should be restricted, so we'll do the restriction checks
* and return restricted results.
*
* This is run if we have a restriction and a reason for restriction
* and we either don't have a logged in student or the logged in student doesn't have access.
*/
if ( ! empty( $restriction_id ) && ! empty( $reason ) && ( ! $student || ! $student->is_enrolled( $restriction_id ) ) ) {
$results['is_restricted'] = true;
$results['reason'] = $reason;
$results['restriction_id'] = $restriction_id;
/**
* Allow filtering of the restricted results.
*
* @since Unknown
*
* @param array $results Restriction check result data.
* @param int $post_id WordPress Post ID of the content.
*/
return apply_filters( 'llms_page_restricted', $results, $post_id );
}
/**
* At this point student has access or the content isn't supposed to be restricted
* we need to do some additional checks for specific post types.
*/
if ( is_singular() ) {
if ( 'llms_quiz' === $post_type ) {
$quiz_id = llms_is_quiz_accessible( $post_id, $user_id );
if ( $quiz_id ) {
$results['is_restricted'] = true;
$results['reason'] = 'quiz';
$results['restriction_id'] = $post_id;
/* This filter is documented above. */
return apply_filters( 'llms_page_restricted', $results, $post_id );
}
}
if ( 'lesson' === $post_type || 'llms_quiz' === $post_type ) {
$course_id = llms_is_post_restricted_by_time_period( $post_id, $user_id );
if ( $course_id ) {
$results['is_restricted'] = true;
$results['reason'] = 'course_time_period';
$results['restriction_id'] = $course_id;
/* This filter is documented above. */
return apply_filters( 'llms_page_restricted', $results, $post_id );
}
$prereq_data = llms_is_post_restricted_by_prerequisite( $post_id, $user_id );
if ( $prereq_data ) {
$results['is_restricted'] = true;
$results['reason'] = sprintf( '%s_prerequisite', $prereq_data['type'] );
$results['restriction_id'] = $prereq_data['id'];
/* This filter is documented above. */
return apply_filters( 'llms_page_restricted', $results, $post_id );
}
$lesson_id = llms_is_post_restricted_by_drip_settings( $post_id, $user_id );
if ( $lesson_id ) {
$results['is_restricted'] = true;
$results['reason'] = 'lesson_drip';
$results['restriction_id'] = $lesson_id;
/* This filter is documented above. */
return apply_filters( 'llms_page_restricted', $results, $post_id );
}
}
}
/* This filter is documented above. */
return apply_filters( 'llms_page_restricted', $results, $post_id );
}
/**
* Retrieve a message describing the reason why content is restricted.
* Accepts an associative array of restriction data that can be retrieved from llms_page_restricted().
*
* This function doesn't handle all restriction types but it should in the future.
* Currently it's being utilized for tooltips on lesson previews and some messages
* output during LLMS_Template_Loader handling redirects.
*
* @since 3.2.4
* @since 3.16.12 Unknown.
*
* @param array $restriction Array of data from `llms_page_restricted()`.
* @return string
*/
function llms_get_restriction_message( $restriction ) {
$msg = __( 'You do not have permission to access this content', 'lifterlms' );
switch ( $restriction['reason'] ) {
case 'course_prerequisite':
$lesson = new LLMS_Lesson( $restriction['content_id'] );
$course_id = $restriction['restriction_id'];
$prereq_link = '<a href="' . get_permalink( $course_id ) . '">' . get_the_title( $course_id ) . '</a>';
$msg = sprintf(
/* Translators: %$1s = lesson title; %2$s link of the course prerequisite */
_x(
'The lesson "%1$s" cannot be accessed until the required prerequisite course "%2$s" is completed.',
'restricted by course prerequisite message',
'lifterlms'
),
$lesson->get( 'title' ),
$prereq_link
);
break;
case 'course_track_prerequisite':
$lesson = new LLMS_Lesson( $restriction['content_id'] );
$track = new LLMS_Track( $restriction['restriction_id'] );
$prereq_link = '<a href="' . $track->get_permalink() . '">' . $track->term->name . '</a>';
$msg = sprintf(
/* Translators: %$1s = lesson title; %2$s link of the track prerequisite */
_x(
'The lesson "%1$s" cannot be accessed until the required prerequisite track "%2$s" is completed.',
'restricted by course track prerequisite message',
'lifterlms'
),
$lesson->get( 'title' ),
$prereq_link
);
break;
// this particular case is only utilized by lessons, courses do the check differently in the template.
case 'course_time_period':
$course = new LLMS_Course( $restriction['restriction_id'] );
// if the start date hasn't passed yet.
if ( ! $course->has_date_passed( 'start_date' ) ) {
$msg = $course->get( 'course_opens_message' );
} elseif ( $course->has_date_passed( 'end_date' ) ) {
$msg = $course->get( 'course_closed_message' );
}
break;
case 'enrollment_lesson':
$course = new LLMS_Course( $restriction['restriction_id'] );
$msg = $course->get( 'content_restricted_message' );
break;
case 'lesson_drip':
$lesson = new LLMS_Lesson( $restriction['restriction_id'] );
$msg = sprintf(
/* Translators: %$1s = lesson title; %2$s available date */
_x(
'The lesson "%1$s" will be available on %2$s',
'lesson restricted by drip settings message',
'lifterlms'
),
$lesson->get( 'title' ),
$lesson->get_available_date()
);
break;
case 'lesson_prerequisite':
$lesson = new LLMS_Lesson( $restriction['content_id'] );
$prereq_lesson = new LLMS_Lesson( $restriction['restriction_id'] );
$prereq_link = '<a href="' . get_permalink( $prereq_lesson->get( 'id' ) ) . '">' . $prereq_lesson->get( 'title' ) . '</a>';
$msg = sprintf(
/* Translators: %$1s = lesson title; %2$s link of the lesson prerequisite */
_x(
'The lesson "%1$s" cannot be accessed until the required prerequisite "%2$s" is completed.',
'lesson restricted by prerequisite message',
'lifterlms'
),
$lesson->get( 'title' ),
$prereq_link
);
break;
default:
}
/**
* Allow filtering the restriction message.
*
* @since Unknown
*
* @param string $msg Restriction message.
* @param array $restriction Array of data from `llms_page_restricted()`.
*/
return apply_filters( 'llms_get_restriction_message', do_shortcode( $msg ), $restriction );
}
/**
* Get a boolean out of llms_page_restricted for easy if checks.
*
* @since 3.0.0
* @since 3.37.10 Made `$user_id` parameter optional. Default is `null`.
*
* @param int $post_id WordPress Post ID of the content.
* @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.
* @return bool
*/
function llms_is_page_restricted( $post_id, $user_id = null ) {
$restrictions = llms_page_restricted( $post_id, $user_id );
return $restrictions['is_restricted'];
}
/**
* Determine if a lesson/quiz is restricted by drip settings.
*
* @since 3.0.0
* @since 3.16.11 Unknown.
* @since 3.37.10 Use strict comparison '===' in place of '=='.
* @since 6.5.0 Improve code readability turning if-elseif into a switch-case.
* Bypass drip content restriction on already completed lessons.
*
* @param int $post_id WP Post ID of a lesson or quiz.
* @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.
* @return int|false False if the lesson is available.
* WP Post ID of the lesson if it is not.
*/
function llms_is_post_restricted_by_drip_settings( $post_id, $user_id = null ) {
$post_type = get_post_type( $post_id );
switch ( $post_type ) {
// If we're on a lesson, lesson id is the post id.
case 'lesson':
$lesson_id = $post_id;
break;
case 'llms_quiz':
$quiz = llms_get_post( $post_id );
$lesson_id = $quiz->get( 'lesson_id' );
if ( ! $lesson_id ) {
return false;
}
break;
default: // Don't pass other post types.
return false;
}
$lesson = new LLMS_Lesson( $lesson_id );
$user_id = $user_id ?? get_current_user_id();
/**
* Filters whether or not to bypass drip restrictions on completed lessons.
*
* @since 6.5.0
*
* @param boolean $drip_bypass Whether or not to bypass drip restrictions on completed lessons.
* @param int $post_id WP Post ID of a lesson or quiz potentially restricted by drip settings.
* @param int $user_id WP User ID.
*/
$drip_bypass = apply_filters( 'llms_lesson_drip_bypass_if_completed', true, $post_id, $user_id );
$is_available = ( $drip_bypass && $user_id && llms_is_complete( $user_id, $lesson_id, 'lesson' ) ) || $lesson->is_available();
return $is_available ? false : $lesson_id;
}
/**
* Determine if a lesson/quiz is restricted by a prerequisite lesson.
*
* @since 3.0.0
* @since 3.16.11 Unknown.
* @since 6.5.0 Improve code readability turning if-elseif into a switch-case.
*
* @param int $post_id WP Post ID of a lesson or quiz.
* @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.
* @return array|false False if the post is not restricted or the user has completed the prereq
* associative array with prereq type and prereq id
* array(
* type => [course|course_track|lesson]
* id => int (object id)
* ).
*/
function llms_is_post_restricted_by_prerequisite( $post_id, $user_id = null ) {
$post_type = get_post_type( $post_id );
switch ( $post_type ) {
// If we're on a lesson, lesson id is the post id.
case 'lesson':
$lesson_id = $post_id;
break;
case 'llms_quiz':
$quiz = llms_get_post( $post_id );
$lesson_id = $quiz->get( 'lesson_id' );
if ( ! $lesson_id ) {
return false;
}
break;
default: // Don't pass other post types.
return false;
}
$lesson = llms_get_post( $lesson_id );
$course = $lesson->get_course();
if ( ! $course ) {
return false;
}
// Get an array of all possible prerequisites.
$prerequisites = array();
if ( $course->has_prerequisite( 'course' ) ) {
$prerequisites[] = array(
'id' => $course->get_prerequisite_id( 'course' ),
'type' => 'course',
);
}
if ( $course->has_prerequisite( 'course_track' ) ) {
$prerequisites[] = array(
'id' => $course->get_prerequisite_id( 'course_track' ),
'type' => 'course_track',
);
}
if ( $lesson->has_prerequisite() ) {
$prerequisites[] = array(
'id' => $lesson->get_prerequisite(),
'type' => 'lesson',
);
}
// Prerequisites exist and user is not logged in, return the first prereq id.
if ( $prerequisites && ! $user_id ) {
return array_shift( $prerequisites );
// If incomplete, send the prereq id.
} else {
$student = new LLMS_Student( $user_id );
foreach ( $prerequisites as $prereq ) {
if ( ! $student->is_complete( $prereq['id'], $prereq['type'] ) ) {
return $prereq;
}
}
}
// Otherwise return false: no prerequisite.
return false;
}
/**
* Determine if a course (or lesson/quiz) is "open" according to course time period settings.
*
* @since 3.0.0
* @since 3.16.11 Unknown.
* @since 5.7.0 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.
* @since 6.5.0 Improve code readability turning if-elseif into a switch-case.
*
* @param int $post_id WP Post ID of a course, lesson, or quiz.
* @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.
* @return int|false False if the post is not restricted by course time period,
* WP Post ID of the course if it is.
*/
function llms_is_post_restricted_by_time_period( $post_id, $user_id = null ) {
$post_type = get_post_type( $post_id );
switch ( $post_type ) {
// If we're on a lesson, get course information.
case 'lesson':
$lesson = new LLMS_Lesson( $post_id );
$course_id = $lesson->get( 'parent_course' );
break;
case 'llms_quiz':
$quiz = llms_get_post( $post_id );
$lesson_id = $quiz->get( 'lesson_id' );
if ( ! $lesson_id ) {
return false;
}
$lesson = llms_get_post( $lesson_id );
if ( ! $lesson_id ) {
return false;
}
$course_id = $lesson->get( 'parent_course' );
break;
case 'course':
$course_id = $post_id;
break;
default: // Don't pass other post types.
return false;
}
$course = new LLMS_Course( $course_id );
return $course->is_open() ? false : $course_id;
}
/**
* Determine if a WordPress post (of any type) is restricted to at least one LifterLMS Membership level.
*
* This function replaces the now deprecated page_restricted_by_membership() (and has slightly different functionality).
*
* @since 3.0.0
* @since 3.16.14 Unknown.
* @since 3.37.10 Call `in_array()` with strict comparison.
*
* @param int $post_id WP_Post ID.
* @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.
* @return bool|int WP_Post ID of the membership if a restriction is found.
* False if no restrictions found.
*/
function llms_is_post_restricted_by_membership( $post_id, $user_id = null ) {
// don't check these posts types.
$skip = apply_filters(
'llms_is_post_restricted_by_membership_skip_post_types',
array(
'course',
'lesson',
'llms_quiz',
'llms_membership',
'llms_question',
'llms_certificate',
'llms_my_certificate',
)
);
if ( in_array( get_post_type( $post_id ), $skip, true ) ) {
return false;
}
$memberships = get_post_meta( $post_id, '_llms_restricted_levels', true );
$restricted = get_post_meta( $post_id, '_llms_is_restricted', true );
if ( 'yes' === $restricted && $memberships && is_array( $memberships ) ) {
// if no user, return the first membership from the array as the restriction id.
if ( ! $user_id ) {
$restriction_id = array_shift( $memberships );
} else {
$student = llms_get_student( $user_id );
if ( ! $student ) {
$restriction_id = array_shift( $memberships );
} else {
// reverse so to ensure that if user is in none of the memberships,
// they'd encounter the same restriction settings as a visitor.
$memberships = array_reverse( $memberships );
// loop through the memberships.
foreach ( $memberships as $mid ) {
// set this as the restriction id.
$restriction_id = $mid;
// once we find the student has access break the loop,
// this will be the restriction that the template loader will check against later.
if ( $student->is_enrolled( $mid ) ) {
break;
}
}
}
}
return absint( $restriction_id );
}
return false;
}
/**
* Determine if a post should bypass sitewide membership restrictions.
*
* If sitewide membership restriction is disabled, this will always return false.
* This function replaces the now deprecated site_restricted_by_membership() (and has slightly different functionality).
*
* @since 3.0.0
* @since 3.37.10 Do not apply membership restrictions on the page set as membership's restriction redirect page.
* Exclude the privacy policy from the sitewide restriction.
* Call `in_array()` with strict comparison.
*
* @param int $post_id WP Post ID.
* @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.
* @return bool|int If the post is not restricted (or there are not sitewide membership restrictions) returns false.
* If the post is restricted, returns the membership id required.
*/
function llms_is_post_restricted_by_sitewide_membership( $post_id, $user_id = null ) {
$membership_id = absint( get_option( 'lifterlms_membership_required', '' ) );
// site is restricted to a membership.
if ( ! empty( $membership_id ) ) {
$membership = new LLMS_Membership( $membership_id );
if ( ! $membership || ! is_a( $membership, 'LLMS_Membership' ) ) {
return false;
}
// Restricted contents redirection page id, if any.
$redirect_page_id = 'page' === $membership->get( 'restriction_redirect_type' ) ? absint( $membership->get( 'redirect_page_id' ) ) : 0;
/**
* Pages that can be bypassed when sitewide restrictions are enabled.
*/
$allowed = apply_filters(
'lifterlms_sitewide_restriction_bypass_ids',
array_filter(
array(
absint( $membership_id ), // the membership page the site is restricted to.
absint( get_option( 'lifterlms_terms_page_id' ) ), // terms and conditions.
llms_get_page_id( 'memberships' ), // membership archives.
llms_get_page_id( 'myaccount' ), // lifterlms account page.
llms_get_page_id( 'checkout' ), // lifterlms checkout page.
absint( get_option( 'wp_page_for_privacy_policy' ) ), // wp privacy policy page.
$redirect_page_id, // Restricted contents redirection page id.
)
)
);
if ( in_array( $post_id, $allowed, true ) ) {
return false;
}
return $membership_id;
} else {
return false;
}
}
/**
* Determine if a quiz should be accessible by a user.
*
* @since 3.1.6
* @since 3.16.1 Unknown.
*
* @param int $post_id WP Post ID.
* @param int|null $user_id Optional. WP User ID (will use get_current_user_id() if none supplied). Default `null`.
* @return bool|int If the post is not restricted returns false.
* If the post is restricted, returns the quiz id.
*/
function llms_is_quiz_accessible( $post_id, $user_id = null ) {
$quiz = llms_get_post( $post_id );
$lesson_id = $quiz->get( 'lesson_id' );
// No lesson or the user is not enrolled.
if ( ! $lesson_id || ! llms_is_user_enrolled( $user_id, $lesson_id ) ) {
return $post_id;
}
return false;
}