Source: includes/sensei-functions.php

<?php
/**
 * Global Sensei functions
 */

/**
 * Determine if the current page is a Sensei LMS page.
 *
 * @since 1.5.0
 *
 * @return bool True if current page is a Sensei LMS page.
 */
function is_sensei() {
	global $post;

	$is_sensei = false;

	$post_types = array( 'lesson', 'course', 'quiz', 'question', 'sensei_message' );
	$taxonomies = array( 'course-category', 'quiz-type', 'question-type', 'lesson-tag', 'module' );

	if ( is_post_type_archive( $post_types )
		|| is_singular( $post_types )
		|| is_tax( $taxonomies )
	) {
		$is_sensei = true;
	} elseif ( is_object( $post ) && ! is_wp_error( $post ) ) {
		$course_page_id           = intval( Sensei()->settings->settings['course_page'] );
		$my_courses_page_id       = Sensei()->settings->get_my_courses_page_id();
		$course_completed_page_id = intval( Sensei()->settings->settings['course_completed_page'] );

		if ( ! empty( $post->ID ) && in_array( $post->ID, [ $course_page_id, $my_courses_page_id, $course_completed_page_id ], true ) ) {
			$is_sensei = true;
		}

		if ( Sensei_Utils::is_learner_profile_page()
			|| Sensei_Utils::is_course_results_page()
			|| Sensei_Utils::is_teacher_archive_page()
			|| Sensei()->blocks->has_sensei_blocks()
		) {
			$is_sensei = true;
		}
	}

	return apply_filters( 'is_sensei', $is_sensei, $post );
}

/**
 * Determine if a user is an admin that can access all of Sensei without restrictions or if he is a teacher accessing
 * his own course.
 *
 * @since 1.4.0
 * @since 3.0.0 Added `$user_id` argument. Preserves backward compatibility.
 *
 * @param int $user_id User ID. Defaults to current user.
 *
 * @return boolean
 */
function sensei_all_access( $user_id = null ) {
	if ( null === $user_id ) {
		$user_id = get_current_user_id();
	}

	if ( empty( $user_id ) ) {
		return false;
	}

	$access = false;

	if ( user_can( $user_id, 'manage_sensei' ) ) {
		$access = true;
	} else {
		$course_id = Sensei_Utils::get_current_course();

		if ( $course_id ) {
			$teacher = (int) get_post( $course_id )->post_author;
			$access  = $user_id === $teacher;
		}
	}

	if ( has_filter( 'sensei_all_access' ) ) {
		// For backwards compatibility with filter, we temporarily need to change the current user.
		$previous_current_user_id = get_current_user_id();
		wp_set_current_user( $user_id );

		/**
		 * Filter sensei_all_access function result which determines if the current user
		 * can access all of Sensei without restrictions.
		 *
		 * @since 1.4.0
		 * @deprecated 3.0.0
		 *
		 * @param bool $access True if user has all access.
		 */
		$access = apply_filters_deprecated( 'sensei_all_access', [ $access ], '3.0.0', 'sensei_user_all_access' );

		wp_set_current_user( $previous_current_user_id );
	}

	/**
	 * Filter if a particular user has access to all of Sensei without restrictions.
	 *
	 * @since 3.0.0
	 *
	 * @hooks sensei_user_all_access
	 *
	 * @param {bool} $access  True if user has all access.
	 * @param {int}  $user_id User ID to check.
	 * @return {bool} Filtered access.
	 */
	return apply_filters( 'sensei_user_all_access', $access, $user_id );
}

/**
 * Function to determine if the current user can
 * access the current lesson content being viewed.
 *
 * This function checks in the following order
 * - if the current user has all access based on their permissions
 * - If the access permission setting is enabled for this site, if not the user has access
 * - if the lesson has a pre-requisite and if the user has completed that
 * - If it is a preview the user has access as well
 *
 * @since 1.9.0
 *
 * @param int $lesson_id Lesson post ID. Default: Use global post in loop.
 * @param int $user_id   User ID. Default: Use currently logged in user ID.
 * @return bool
 */
function sensei_can_user_view_lesson( $lesson_id = null, $user_id = null ) {
	if ( empty( $lesson_id ) ) {
		$lesson_id = get_the_ID();
	}

	$context = 'lesson';
	if ( 'quiz' === get_post_type( get_the_ID() ) ) {
		$context   = 'quiz';
		$lesson_id = Sensei()->quiz->get_lesson_id( get_the_ID() );
	}

	if ( empty( $user_id ) ) {
		$user_id = get_current_user_id();
	}

	$user_can_view_course_content = false;
	$course_id                    = Sensei()->lesson->get_course_id( $lesson_id );
	if ( $course_id ) {
		$user_can_view_course_content = Sensei()->course->can_access_course_content( $course_id, $user_id, $context );
	}

	// Check for prerequisite lesson completions.
	$pre_requisite_complete = Sensei_Lesson::is_prerequisite_complete( $lesson_id, $user_id );
	$is_preview_lesson      = false;

	if ( $lesson_id && Sensei_Utils::is_preview_lesson( $lesson_id ) ) {
		$is_preview_lesson      = true;
		$pre_requisite_complete = true;
	};

	$login_not_required  = ! sensei_is_login_required();
	$user_has_all_access = sensei_all_access( $user_id );

	$can_user_view_lesson = $login_not_required
							|| $user_has_all_access
							|| ( $user_can_view_course_content && $pre_requisite_complete )
							|| $is_preview_lesson;

	$checks = array(
		'login_not_required'           => $login_not_required,
		'user_has_all_access'          => $user_has_all_access,
		'user_can_view_course_content' => $user_can_view_course_content,
		'pre_requisite_complete'       => $pre_requisite_complete,
		'is_preview_lesson'            => $is_preview_lesson,
	);

	/**
	 * Filter if the user can view lesson and quiz content.
	 *
	 * @since 1.9.0
	 * @since 4.24.2 Added $checks parameter.
	 *
	 * @hook sensei_can_user_view_lesson
	 *
	 * @param {bool}  $can_user_view_lesson True if they can view lesson/quiz content.
	 * @param {int}   $lesson_id            Lesson post ID.
	 * @param {int}   $user_id              User ID.
	 * $param {array} $checks               Array of checks that were made to determine access.
	 * @return {bool} Filtered access.
	 */
	return apply_filters( 'sensei_can_user_view_lesson', $can_user_view_lesson, $lesson_id, $user_id, $checks );
}

if ( ! function_exists( 'sensei_light_or_dark' ) ) {

	/**
	 * Detect if we should use a light or dark colour on a background colour
	 *
	 * @access public
	 * @param mixed  $color
	 * @param string $dark (default: '#000000')
	 * @param string $light (default: '#FFFFFF')
	 * @return string
	 */
	function sensei_light_or_dark( $color, $dark = '#000000', $light = '#FFFFFF' ) {

		$hex = str_replace( '#', '', $color );

		$c_r        = hexdec( substr( $hex, 0, 2 ) );
		$c_g        = hexdec( substr( $hex, 2, 2 ) );
		$c_b        = hexdec( substr( $hex, 4, 2 ) );
		$brightness = ( ( $c_r * 299 ) + ( $c_g * 587 ) + ( $c_b * 114 ) ) / 1000;

		return $brightness > 155 ? $dark : $light;
	}
}

if ( ! function_exists( 'sensei_rgb_from_hex' ) ) {

	/**
	 * Hex darker/lighter/contrast functions for colours
	 *
	 * @access public
	 * @param mixed $color
	 * @return string
	 */
	function sensei_rgb_from_hex( $color ) {
		$color = str_replace( '#', '', $color );
		// Convert shorthand colors to full format, e.g. "FFF" -> "FFFFFF"
		$color = preg_replace( '~^(.)(.)(.)$~', '$1$1$2$2$3$3', $color );

		$rgb      = [];
		$rgb['R'] = hexdec( $color[0] . $color[1] );
		$rgb['G'] = hexdec( $color[2] . $color[3] );
		$rgb['B'] = hexdec( $color[4] . $color[5] );
		return $rgb;
	}
}

if ( ! function_exists( 'sensei_hex_darker' ) ) {

	/**
	 * Hex darker/lighter/contrast functions for colours
	 *
	 * @access public
	 * @param mixed $color
	 * @param int   $factor (default: 30)
	 * @return string
	 */
	function sensei_hex_darker( $color, $factor = 30 ) {
		$base  = sensei_rgb_from_hex( $color );
		$color = '#';

		foreach ( $base as $k => $v ) :
			$amount      = $v / 100;
			$amount      = round( $amount * $factor );
			$new_decimal = $v - $amount;

			$new_hex_component = dechex( $new_decimal );
			if ( strlen( $new_hex_component ) < 2 ) :
				$new_hex_component = '0' . $new_hex_component;
			endif;
			$color .= $new_hex_component;
		endforeach;

		return $color;
	}
}

if ( ! function_exists( 'sensei_hex_lighter' ) ) {

	/**
	 * Hex darker/lighter/contrast functions for colours
	 *
	 * @access public
	 * @param mixed $color
	 * @param int   $factor (default: 30)
	 * @return string
	 */
	function sensei_hex_lighter( $color, $factor = 30 ) {
		$base  = sensei_rgb_from_hex( $color );
		$color = '#';

		foreach ( $base as $k => $v ) :
			$amount      = 255 - $v;
			$amount      = $amount / 100;
			$amount      = round( $amount * $factor );
			$new_decimal = $v + $amount;

			$new_hex_component = dechex( $new_decimal );
			if ( strlen( $new_hex_component ) < 2 ) :
				$new_hex_component = '0' . $new_hex_component;
			endif;
			$color .= $new_hex_component;
		endforeach;

		return $color;
	}
}

/**
 * Provides an interface to allow us to deprecate hooks while still allowing them
 * to work, but giving the developer an error message.
 *
 * @since 1.9.0
 *
 * @param $hook_tag
 * @param $version
 * @param $alternative
 * @param array       $args
 */
function sensei_do_deprecated_action( $hook_tag, $version, $alternative = '', $args = array() ) {

	if ( has_action( $hook_tag ) ) {

		// translators: Placeholders are the hook tag and the version which it was deprecated, respectively.
		$error_message = sprintf( __( "SENSEI: The hook '%1\$s', has been deprecated since '%2\$s'.", 'sensei-lms' ), $hook_tag, $version );

		if ( ! empty( $alternative ) ) {

			// translators: Placeholder is the alternative action name.
			$error_message .= sprintf( __( "Please use '%s' instead.", 'sensei-lms' ), $alternative );

		}

		trigger_error( esc_html( $error_message ) );
		do_action( $hook_tag, $args );

	}

}

/**
 * Check the given post or post type id is a of the
 * the course post type.
 *
 * @since 1.9.0
 *
 * @param $post_id
 * @return bool
 */
function sensei_is_a_course( $post ) {

	return 'course' == get_post_type( $post );

}

/**
 * Get registration url.
 *
 * @since 3.15.0
 *
 * @param bool   $return_wp_registration_url Whether return the url if it should use the WP registration url.
 * @param string $redirect                   Redirect url after registration.
 *
 * @return string|null The registration url.
 *                     If wp registration is the return case and $return_wp_registration_url is
 *                     true, it returns the url, otherwise it returns null.
 */
function sensei_user_registration_url( bool $return_wp_registration_url = true, string $redirect = '' ) {
	/**
	 * Filter to force Sensei to output the default WordPress user registration link.
	 *
	 * @since 1.9.0
	 *
	 * @hook sensei_use_wp_register_link
	 *
	 * @param {bool} $wp_register_link Whether to use the default WordPress registration link, default: false.
	 * @return {bool} Filtered value.
	 */
	$wp_register_link   = apply_filters( 'sensei_use_wp_register_link', false );
	$registration_url   = '';
	$my_courses_page_id = Sensei()->settings->get_my_courses_page_id();

	if ( empty( $my_courses_page_id ) || $wp_register_link ) {
		if ( ! $return_wp_registration_url ) {
			return null;
		}

		$registration_url = wp_registration_url();
	} else {
		$registration_url = get_permalink( $my_courses_page_id );
	}

	if ( ! empty( $redirect ) ) {
		$registration_url = add_query_arg( 'redirect_to', $redirect, $registration_url );
	}

	/**
	 * Filter the registration URL.
	 *
	 * @since 4.4.1
	 *
	 * @hook sensei_registration_url
	 *
	 * @param {string} $registration_url Registration URL.
	 * @param {string} $redirect         Redirect url after registration.
	 * @return {string} Returns filtered registration URL.
	 */
	return apply_filters( 'sensei_registration_url', $registration_url, $redirect );
}

/**
 * Determine the login link
 * on the frontend.
 *
 * This function will return the my-courses page link
 * or the wp-login link.
 *
 * @since 1.9.0
 * @since 3.15.0 Introduce redirect param.
 *
 * @param string $redirect Redirect url after login.
 *
 * @return string The login url.
 */
function sensei_user_login_url( string $redirect = '' ) {
	$login_url          = '';
	$my_courses_page_id = Sensei()->settings->get_my_courses_page_id();
	$page               = get_post( $my_courses_page_id );

	if ( $my_courses_page_id && isset( $page->ID ) && 'page' == get_post_type( $page->ID ) ) {
		$my_courses_url = get_permalink( $page->ID );
		if ( ! empty( $redirect ) ) {
			$my_courses_url = add_query_arg( 'redirect_to', $redirect, $my_courses_url );
		}

		$login_url = $my_courses_url;
	} else {
		$login_url = wp_login_url( $redirect );
	}

	/**
	 * Filter the login URL.
	 *
	 * @since 4.4.1
	 *
	 * @hook sensei_login_url
	 *
	 * @param {string} $login_url Login URL.
	 * @param {string} $redirect  Redirect url after login.
	 * @return {string} Returns filtered login URL.
	 */
	return apply_filters( 'sensei_login_url', $login_url, $redirect );
}

/**
 * Checks the settings to see
 * if a user must be logged in to view content
 *
 * duplicate of Sensei()->access_settings().
 *
 * @since 1.9.0
 * @since 3.5.2 Added hook to filter the return.
 * @return bool
 */
function sensei_is_login_required() {
	$course_id = Sensei_Utils::get_current_course();

	$login_required = isset( Sensei()->settings->settings['access_permission'] ) && ( true == Sensei()->settings->settings['access_permission'] );

	/**
	 * Filters the access_permission that says if the user must be logged to view the lesson content.
	 *
	 * @since 3.5.2
	 *
	 * @hook sensei_is_login_required
	 *
	 * @param {bool}     $must_be_logged_to_view_lesson True if user need to be logged to see the lesson.
	 * @param {int|null} $course_id                     Course post ID.
	 * @return {bool} Whether the user needs to be logged in to view content.
	 */
	return apply_filters( 'sensei_is_login_required', $login_required, $course_id );
}

/**
 * Checks if this theme supports Sensei templates.
 *
 * @since 1.12.0
 * @return bool
 */
function sensei_does_theme_support_templates() {
	$current_theme = wp_get_theme()->get_template();
	$themes        = Sensei()->theme_integration_loader->get_supported_themes();

	return in_array( $current_theme, $themes, true ) || current_theme_supports( 'sensei' );
}

/**
 * Track a Sensei event.
 *
 * @since 2.1.0
 *
 * @param string $event_name The name of the event, without the `sensei_` prefix.
 * @param array  $properties The event properties to be sent.
 */
function sensei_log_event( $event_name, $properties = [] ) {
	$properties = array_merge(
		Sensei_Usage_Tracking_Data::get_event_logging_base_fields(),
		$properties
	);

	/**
	 * Explicitly disable usage tracking from being sent.
	 *
	 * @since 2.1.0
	 *
	 * @hook sensei_log_event
	 *
	 * @param {bool}   $log_event    Whether we should log the event.
	 * @param {string} $event_name   The name of the event, without the `sensei_` prefix.
	 * @param {array}  $properties   The event properties to be sent.
	 * @return {bool} Whether we should log the event.
	 */
	if ( false === apply_filters( 'sensei_log_event', true, $event_name, $properties ) ) {
		return;
	}

	Sensei_Usage_Tracking::get_instance()->send_event( $event_name, $properties );
}

/**
 * Track a Sensei event with Jetpack, when Jetpack is available.
 *
 * @since 3.7.0
 *
 * @param string $event_name The name of the event, without the `sensei_` prefix.
 * @param array  $properties The event properties to be sent.
 */
function sensei_log_jetpack_event( $event_name, $properties = [] ) {
	if ( ! class_exists( 'Automattic\Jetpack\Tracking' ) || ! Sensei()->usage_tracking->is_tracking_enabled() ) {
		return;
	}

	$jetpack_connection = Jetpack::connection();
	if ( $jetpack_connection->is_user_connected() ) {
		$tracking = new Automattic\Jetpack\Tracking( 'sensei', $jetpack_connection );
		$tracking->record_user_event( $event_name, $properties );
	}
}