<?php
use Sensei\Internal\Student_Progress\Course_Progress\Models\Course_Progress_Interface;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly
}
/**
* Sensei Utilities Class
*
* Common utility functions for Sensei.
*
* @package Core
* @author Automattic
*
* @since 1.0.0
*/
class Sensei_Utils {
const WC_INFORMATION_TRANSIENT = 'sensei_woocommerce_plugin_information';
/**
* Get the placeholder thumbnail image.
*
* @access public
* @since 1.0.0
* @return string The URL to the placeholder thumbnail image.
*/
public static function get_placeholder_image() {
/**
* Filter the placeholder thumbnail image.
*
* @hook sensei_placeholder_thumbnail
*
* @param {string} $placeholder_image_url The URL to the placeholder thumbnail image.
* @return {string} The URL to the placeholder thumbnail image.
*/
return esc_url( apply_filters( 'sensei_placeholder_thumbnail', Sensei()->plugin_url . 'assets/images/placeholder.png' ) );
}
/**
* Log an activity item.
*
* @access public
* @since 1.0.0
* @param array $args (default: array())
* @return bool | int
*/
public static function sensei_log_activity( $args = array() ) {
global $wpdb;
// Args, minimum data required for WP
$data = array(
'comment_post_ID' => intval( $args['post_id'] ),
'comment_author' => '', // Not needed
'comment_author_email' => '', // Not needed
'comment_author_url' => '', // Not needed
'comment_content' => ! empty( $args['data'] ) ? esc_html( $args['data'] ) : '',
'comment_type' => esc_attr( $args['type'] ),
'user_id' => intval( $args['user_id'] ),
'comment_approved' => ! empty( $args['status'] ) ? esc_html( $args['status'] ) : 'log',
);
// Allow extra data
if ( ! empty( $args['username'] ) ) {
$data['comment_author'] = sanitize_user( $args['username'] );
}
if ( ! empty( $args['user_email'] ) ) {
$data['comment_author_email'] = sanitize_email( $args['user_email'] );
}
if ( ! empty( $args['user_url'] ) ) {
$data['comment_author_url'] = esc_url( $args['user_url'] );
}
if ( ! empty( $args['parent'] ) ) {
$data['comment_parent'] = $args['parent'];
}
// Sanity check
if ( empty( $args['user_id'] ) ) {
_deprecated_argument( __FUNCTION__, '1.0', esc_html__( 'At no point should user_id be equal to 0.', 'sensei-lms' ) );
return false;
}
/**
* This action runs before logging the activity.
*
* @hook sensei_log_activity_before
*
* @param {array} $args Initial arguments.
* @param {array} $data Processed data to log.
*/
do_action( 'sensei_log_activity_before', $args, $data );
// Custom Logic
// Check if comment exists first
$comment_id = $wpdb->get_var( $wpdb->prepare( "SELECT comment_ID FROM $wpdb->comments WHERE comment_post_ID = %d AND user_id = %d AND comment_type = %s ", $args['post_id'], $args['user_id'], $args['type'] ) );
if ( ! $comment_id ) {
// Add the comment
$comment_id = wp_insert_comment( $data );
} elseif ( isset( $args['action'] ) && 'update' == $args['action'] ) {
// Update the comment if an update was requested
$data['comment_ID'] = $comment_id;
// By default update the timestamp of the comment
if ( empty( $args['keep_time'] ) ) {
$data['comment_date'] = current_time( 'mysql' );
}
wp_update_comment( $data );
}
/**
* Fires after logging the activity.
*
* @hook sensei_log_activity_after
*
* @param {array} $args Initial arguments.
* @param {array} $data Processed data to log.
* @param {int} $comment_id The comment ID of the logged activity.
*/
do_action( 'sensei_log_activity_after', $args, $data, $comment_id );
Sensei()->flush_comment_counts_cache( $args['post_id'] );
if ( 0 < $comment_id ) {
// Return the ID so that it can be used for meta data storage
return $comment_id;
} else {
return false;
}
}
/**
* Check for Sensei activity.
*
* @access public
* @since 1.0.0
* @param array $args (default: array())
* @param bool $return_comments (default: false)
* @return mixed | int
*/
public static function sensei_check_for_activity( $args = array(), $return_comments = false ) {
if ( ! $return_comments ) {
$args['count'] = true;
}
// A user ID of 0 is invalid, so shortcut this.
if ( isset( $args['user_id'] ) && 0 === intval( $args['user_id'] ) ) {
_deprecated_argument( __FUNCTION__, '1.0', esc_html__( 'At no point should user_id be equal to 0.', 'sensei-lms' ) );
return false;
}
if ( ! isset( $args['status'] ) ) {
$args['status'] = 'any';
}
/**
* This action runs before getting the comments for the given request.
*
* @hook sensei_utils_check_for_activity_before_get_comments
*
* @param {array} $args Search arguments.
*/
do_action( 'sensei_utils_check_for_activity_before_get_comments', $args );
/**
* This filter runs inside Sensei_Utils::sensei_check_for_activity
*
* It runs while getting the comments for the given request.
*
* @hook sensei_check_for_activity
*
* @param {int|array} $comments Activity to filter.
* @param {array} $args Search arguments.
* @return {int|array} Filtered activity.
*/
$comments = apply_filters( 'sensei_check_for_activity', get_comments( $args ), $args );
/**
* This action runs after getting the comments for the given request.
*
* @hook sensei_utils_check_for_activity_after_get_comments
*
* @param {array} $args Search arguments.
* @param {int|array} $comments Activity.
*/
do_action( 'sensei_utils_check_for_activity_after_get_comments', $args, $comments );
// Return comments.
if ( $return_comments ) {
// Could check for array of 1 and just return the 1 item?
if ( is_array( $comments ) && 1 == count( $comments ) ) {
$comments = array_shift( $comments );
}
return $comments;
}
// Count comments.
return intval( $comments ); // This is the count, check the return from WP_Comment_Query.
}
/**
* Get IDs of Sensei activity items.
*
* @access public
* @since 1.0.0
* @param array $args (default: array())
* @return array
*/
public static function sensei_activity_ids( $args = array() ) {
$comments = self::sensei_check_for_activity( $args, true );
// Need to always use an array, even with only 1 item
if ( ! is_array( $comments ) ) {
$comments = array( $comments );
}
$post_ids = array();
// Count comments
if ( is_array( $comments ) && ( 0 < intval( count( $comments ) ) ) ) {
foreach ( $comments as $key => $value ) {
// Add matches to id array
if ( isset( $args['field'] ) && 'comment' == $args['field'] ) {
array_push( $post_ids, $value->comment_ID );
} elseif ( isset( $args['field'] ) && 'user_id' == $args['field'] ) {
array_push( $post_ids, $value->user_id );
} else {
array_push( $post_ids, $value->comment_post_ID );
}
}
// Reset array indexes
$post_ids = array_unique( $post_ids );
$post_ids = array_values( $post_ids );
}
return $post_ids;
}
/**
* Delete Sensei activities.
*
* @access public
* @since 1.0.0
* @param array $args (default: array())
* @return boolean
*/
public static function sensei_delete_activities( $args = array() ) {
$dataset_changes = false;
// If activity exists remove activity from log
$comments = self::sensei_check_for_activity(
array(
'post_id' => intval( $args['post_id'] ),
'user_id' => intval( $args['user_id'] ),
'type' => esc_attr( $args['type'] ),
),
true
);
if ( $comments ) {
// Need to always return an array, even with only 1 item
if ( ! is_array( $comments ) ) {
$comments = array( $comments );
}
foreach ( $comments as $key => $value ) {
if ( isset( $value->comment_ID ) && 0 < $value->comment_ID ) {
$dataset_changes = wp_delete_comment( intval( $value->comment_ID ), true );
}
}
}
Sensei()->flush_comment_counts_cache( $args['post_id'] );
return $dataset_changes;
}
/**
* Delete all activity for specified user.
*
* @access public
* @since 1.5.0
*
* @deprecated 3.0.0 Use `\Sensei_Learner::delete_all_user_activity` instead.
*
* @param integer $user_id User ID.
* @return boolean
*/
public static function delete_all_user_activity( $user_id = 0 ) {
_deprecated_function( __METHOD__, '3.0.0', 'Sensei_Learner::delete_all_user_activity' );
return \Sensei_Learner::instance()->delete_all_user_activity( $user_id );
}
/**
* Get value for a specified activity.
*
* @access public
* @since 1.0.0
* @param array $args (default: array())
* @return string
*/
public static function sensei_get_activity_value( $args = array() ) {
$activity_value = false;
if ( ! empty( $args['field'] ) ) {
$comment = self::sensei_check_for_activity( $args, true );
if ( isset( $comment->{$args['field']} ) && '' != $comment->{$args['field']} ) {
$activity_value = $comment->{$args['field']};
}
}
return $activity_value;
}
/**
* Load the WordPress rich text editor
*
* @param string $content Initial content for editor
* @param string $editor_id ID of editor (only lower case characters - no spaces, underscores, hyphens, etc.)
* @param string $input_name Name for text area form element
* @return void
*/
public static function sensei_text_editor( $content = '', $editor_id = 'senseitexteditor', $input_name = '' ) {
if ( ! $input_name ) {
$input_name = $editor_id;
}
$buttons = 'bold,italic,underline,strikethrough,blockquote,bullist,numlist,justifyleft,justifycenter,justifyright,undo,redo,pastetext';
$settings = array(
'media_buttons' => false,
'wpautop' => true,
'textarea_name' => $input_name,
'editor_class' => 'sensei_text_editor',
'teeny' => false,
'dfw' => false,
'editor_css' => '<style> .mce-top-part button { background-color: rgba(0,0,0,0); } </style>',
'tinymce' => array(
'theme_advanced_buttons1' => $buttons,
'theme_advanced_buttons2' => '',
'setup' => 'function (editor) {
tinymce.dom.ScriptLoader.ScriptLoader.add("' . Sensei()->assets->asset_url( 'js/question-answer-tinymce-editor.js' ) . '");
tinymce.dom.ScriptLoader.ScriptLoader.loadQueue(function() {
window.addPlaceholderInTinymceEditor(editor);
});
}
',
),
'quicktags' => false,
);
if ( false !== strpos( $input_name, 'sensei_question[' ) ) {
// Only pick the global style variables. TinyMCE loads in an iFrame, so none of our global
// variables are available inside it. We add them here manually.
$global_variables = str_replace( '"', "'", wp_get_global_stylesheet( [ 'variables' ] ) );
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, Squiz.Strings.DoubleQuoteUsage.NotRequired -- Using local file and need double quote for newline.
$question_editor_styles = str_replace( "\n", "", file_get_contents( Sensei()->assets->dist_path( 'css/question-answer-tinymce-editor.css' ) ) );
$settings['tinymce']['content_style'] = $global_variables . ' ' . $question_editor_styles;
}
wp_editor( $content, $editor_id, $settings );
}
public static function upload_file( $file = array() ) {
require_once ABSPATH . 'wp-admin/includes/admin.php';
/**
* Filter the data array for the Sensei wp_handle_upload function call
*
* This filter was mainly added for Unit Testing purposes.
*
* @since 1.7.4
*
* @hook sensei_file_upload_args
*
* @param {array} $file_upload_args Array of current values.
* @property {string} `$file_upload_args['test_form']` Set to false by default.
* @return {array} Filtered data array.
*/
$file_upload_args = apply_filters( 'sensei_file_upload_args', array( 'test_form' => false ) );
/**
* Customize the prefix prepended onto files uploaded in Sensei.
*
* @since 3.9.0
*
* @hook sensei_file_upload_file_prefix
*
* @param {string} $prefix Prefix to prepend to uploaded files.
* @param {array} $file Arguments with uploaded file information.
* @return {string} Filtered prefix.
*/
$file_prefix = apply_filters( 'sensei_file_upload_file_prefix', substr( md5( uniqid() ), 0, 7 ) . '_', $file );
$file['name'] = $file_prefix . $file['name'];
$file_return = wp_handle_upload( $file, $file_upload_args );
if ( isset( $file_return['error'] ) || isset( $file_return['upload_error_handler'] ) ) {
return false;
} else {
$filename = $file_return['file'];
$attachment = array(
'post_mime_type' => $file_return['type'],
'post_title' => preg_replace( '/\.[^.]+$/', '', basename( $filename ) ),
'post_content' => '',
'post_status' => 'inherit',
'guid' => $file_return['url'],
);
$attachment_id = wp_insert_attachment( $attachment, $filename );
require_once ABSPATH . 'wp-admin/includes/image.php';
$attachment_data = wp_generate_attachment_metadata( $attachment_id, $filename );
wp_update_attachment_metadata( $attachment_id, $attachment_data );
if ( 0 < intval( $attachment_id ) ) {
return $attachment_id;
}
}
return false;
}
/**
* Grade quiz
*
* @param integer $quiz_id ID of quiz.
* @param float $grade Grade received.
* @param integer $user_id ID of user being graded.
* @param string $quiz_grade_type default 'auto'.
*
* @return boolean
*/
public static function sensei_grade_quiz( $quiz_id = 0, $grade = 0, $user_id = 0, $quiz_grade_type = 'auto' ): bool {
$user_id = $user_id ? $user_id : get_current_user_id();
if ( ! $quiz_id || ! $user_id ) {
return false;
}
$quiz_submission = Sensei()->quiz_submission_repository->get( $quiz_id, $user_id );
if ( ! $quiz_submission ) {
return false;
}
$quiz_submission->set_final_grade( $grade );
Sensei()->quiz_submission_repository->save( $quiz_submission );
$quiz_passmark = absint( get_post_meta( $quiz_id, '_quiz_passmark', true ) );
/**
* Fires when a user quiz is graded.
*
* @hook sensei_user_quiz_grade
*
* @param {int} $user_id ID of user being graded.
* @param {int} $quiz_id ID of quiz.
* @param {float} $grade Grade received.
* @param {int} $quiz_passmark Passmark for quiz.
* @param {string} $quiz_grade_type Type of grading.
*/
do_action( 'sensei_user_quiz_grade', $user_id, $quiz_id, $grade, $quiz_passmark, $quiz_grade_type );
return true;
}
/**
* Grade question
*
* @deprecated 4.19.2
*
* @param integer $question_id ID of question
* @param integer $grade Grade received
* @param int $user_id
* @return boolean
*/
public static function sensei_grade_question( $question_id = 0, $grade = 0, $user_id = 0 ) {
_deprecated_function( __METHOD__, '4.19.2', 'Sensei_Quiz::set_user_grades' );
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
$activity_logged = false;
if ( intval( $question_id ) > 0 && intval( $user_id ) > 0 ) {
$user_answer_id = self::sensei_get_activity_value(
array(
'post_id' => $question_id,
'user_id' => $user_id,
'type' => 'sensei_user_answer',
'field' => 'comment_ID',
)
);
$activity_logged = update_comment_meta( $user_answer_id, 'user_grade', $grade );
$answer_notes = get_post_meta( $question_id, '_answer_feedback', true );
if ( ! empty( $answer_notes ) ) {
update_comment_meta( $user_answer_id, 'answer_note', base64_encode( $answer_notes ) );
}
}
return $activity_logged;
}
/**
* Delete the question grade.
*
* @deprecated 4.19.2
*
* @param int $question_id The question ID.
* @param int $user_id The user ID. Defaults to the current user ID.
*
* @return bool
*/
public static function sensei_delete_question_grade( $question_id = 0, $user_id = 0 ) {
_deprecated_function( __METHOD__, '4.19.2', 'Sensei_Quiz::set_user_grades' );
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
$activity_logged = false;
if ( intval( $question_id ) > 0 ) {
$user_answer_id = self::sensei_get_activity_value(
array(
'post_id' => $question_id,
'user_id' => $user_id,
'type' => 'sensei_user_answer',
'field' => 'comment_ID',
)
);
$activity_logged = delete_comment_meta( $user_answer_id, 'user_grade' );
}
return $activity_logged;
}
/**
* Alias to Woothemes_Sensei_Utils::sensei_start_lesson
*
* @since 1.7.4
*
* @param integer $user_id
* @param integer $lesson_id
* @param bool $complete
*
* @return mixed boolean or comment_ID
*/
public static function user_start_lesson( $user_id = 0, $lesson_id = 0, $complete = false ) {
return self::sensei_start_lesson( $lesson_id, $user_id, $complete );
}
/**
* Mark a lesson as started for user
*
* Will also start the lesson course for the user if the user hasn't started taking it already.
*
* @since 1.6.0
*
* @param integer $lesson_id ID of lesson
* @param int| string $user_id default 0
* @param bool $complete default false
*
* @return mixed boolean or comment_ID
*/
public static function sensei_start_lesson( $lesson_id = 0, $user_id = 0, $complete = false ) {
if ( 0 === (int) $user_id ) {
$user_id = get_current_user_id();
}
if ( 0 >= (int) $lesson_id ) {
return false;
}
$course_id = get_post_meta( $lesson_id, '_lesson_course', true );
if ( $course_id ) {
$is_user_taking_course = self::has_started_course( $course_id, $user_id );
if ( ! $is_user_taking_course ) {
self::user_start_course( $user_id, $course_id );
}
}
/**
* Fires when a user starts a lesson.
* When this action runs the lesson status may not yet exist.
*
* @hook sensei_user_lesson_start
*
* @param {int} $user_id ID of user starting lesson.
* @param {int} $lesson_id ID of lesson being started.
*/
do_action( 'sensei_user_lesson_start', $user_id, $lesson_id );
$lesson_progress = Sensei()->lesson_progress_repository->get( $lesson_id, $user_id );
if ( ! $lesson_progress ) {
$lesson_progress = Sensei()->lesson_progress_repository->create( $lesson_id, $user_id );
$has_questions = Sensei_Lesson::lesson_quiz_has_questions( $lesson_id );
if ( $complete && $has_questions ) {
update_comment_meta( $lesson_progress->get_id(), 'grade', 0 );
}
}
if ( $complete && ! $lesson_progress->is_complete() ) {
$lesson_progress->complete();
Sensei()->lesson_progress_repository->save( $lesson_progress );
}
if ( $complete ) {
/**
* Fires when a user completes a lesson.
*
* This hook is fired when a user completes a lesson, passes a quiz or their quiz submission was graded.
* Therefore the corresponding lesson is marked as complete.
*
* @since 1.7.0
*
* @hook sensei_user_lesson_end
*
* @param {int} $user_id The user ID.
* @param {int} $lesson_id The lesson ID.
*/
do_action( 'sensei_user_lesson_end', $user_id, $lesson_id );
}
return $lesson_progress->get_id();
}
/**
* Remove user from lesson, deleting all data from the corresponding quiz
*
* @param int $lesson_id The lesson ID.
* @param int $user_id The user ID.
* @param bool $from_course Whether the user is being removed from a course.
* @return boolean
*/
public static function sensei_remove_user_from_lesson( $lesson_id = 0, $user_id = 0, $from_course = false ) {
if ( ! $lesson_id ) {
return false;
}
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
// Process quiz
$lesson_quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id );
// Delete quiz answers, this auto deletes the corresponding meta data, such as the question/answer grade
self::sensei_delete_quiz_answers( $lesson_quiz_id, $user_id );
// Delete quiz saved answers
Sensei()->quiz->reset_user_lesson_data( $lesson_id, $user_id );
// Delete lesson progress.
$lesson_progress = Sensei()->lesson_progress_repository->get( $lesson_id, $user_id );
if ( $lesson_progress ) {
Sensei()->lesson_progress_repository->delete( $lesson_progress );
}
if ( ! $from_course ) {
/**
* Fires when user progress is reset in a lesson.
*
* @hook sensei_user_lesson_reset
*
* @param {int} $user_id The user ID.
* @param {int} $lesson_id The lesson ID.
*/
do_action( 'sensei_user_lesson_reset', $user_id, $lesson_id );
}
return true;
}
/**
* Remove a user from a course, deleting all activities across all lessons
*
* @param int $course_id
* @param int $user_id
* @return boolean
*/
public static function sensei_remove_user_from_course( $course_id = 0, $user_id = 0 ) {
if ( ! $course_id ) {
return false;
}
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
$lesson_ids = Sensei()->course->course_lessons( $course_id, 'any', 'ids' );
foreach ( $lesson_ids as $lesson_id ) {
self::sensei_remove_user_from_lesson( $lesson_id, $user_id, true );
}
// Delete course progress.
$course_progress = Sensei()->course_progress_repository->get( $course_id, $user_id );
if ( $course_progress ) {
Sensei()->course_progress_repository->delete( $course_progress );
}
/**
* Fires when user progress is reset in a course.
*
* @hook sensei_user_course_reset
*
* @param {int} $user_id The user ID.
* @param {int} $course_id The course ID.
*/
do_action( 'sensei_user_course_reset', $user_id, $course_id );
return true;
}
public static function sensei_get_quiz_questions( $quiz_id = 0 ) {
$questions = array();
if ( intval( $quiz_id ) > 0 ) {
$questions = Sensei()->lesson->lesson_quiz_questions( $quiz_id );
$questions = self::array_sort_reorder( $questions );
}
return $questions;
}
public static function sensei_get_quiz_total( $quiz_id = 0 ) {
$quiz_total = 0;
if ( $quiz_id > 0 ) {
$questions = self::sensei_get_quiz_questions( $quiz_id );
$question_grade = 0;
foreach ( $questions as $question ) {
$question_grade = Sensei()->question->get_question_grade( $question->ID );
$quiz_total += $question_grade;
}
}
return $quiz_total;
}
/**
* Returns the user_grade for a specific question and user, or sensei_user_answer entry
*
* @deprecated 4.19.2
*
* @param mixed $question
* @param int $user_id
* @return string
*/
public static function sensei_get_user_question_grade( $question = 0, $user_id = 0 ) {
_deprecated_function( __METHOD__, '4.19.2', 'Sensei_Quiz::get_user_grades' );
$question_grade = false;
if ( $question ) {
if ( is_object( $question ) ) {
$user_answer_id = $question->comment_ID;
} else {
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
$user_answer_id = self::sensei_get_activity_value(
array(
'post_id' => intval( $question ),
'user_id' => $user_id,
'type' => 'sensei_user_answer',
'field' => 'comment_ID',
)
);
}
if ( $user_answer_id ) {
$question_grade = get_comment_meta( $user_answer_id, 'user_grade', true );
}
}
return $question_grade;
}
/**
* Delete the quiz answers and all related data including the grades.
*
* @param int $quiz_id The quiz ID.
* @param int $user_id The user ID.
*
* @return bool
*/
public static function sensei_delete_quiz_answers( $quiz_id = 0, $user_id = 0 ): bool {
if ( intval( $user_id ) === 0 ) {
$user_id = get_current_user_id();
}
if ( ! $quiz_id || ! $user_id ) {
return false;
}
$deleted = false;
$questions = self::sensei_get_quiz_questions( $quiz_id );
foreach ( $questions as $question ) {
// Fallback for pre 1.7.4 data.
$deleted = self::sensei_delete_activities(
array(
'post_id' => $question->ID,
'user_id' => $user_id,
'type' => 'sensei_user_answer',
)
);
}
$quiz_submission = Sensei()->quiz_submission_repository->get( $quiz_id, $user_id );
if ( $quiz_submission ) {
Sensei()->quiz_submission_repository->delete( $quiz_submission );
Sensei()->quiz_answer_repository->delete_all( $quiz_submission );
Sensei()->quiz_grade_repository->delete_all( $quiz_submission );
$deleted = true;
}
return $deleted;
}
/**
* Delete the quiz submission grade.
*
* @param int $quiz_id The quiz ID.
* @param int $user_id The user ID. Defaults to the current user ID.
*
* @return bool
*/
public static function sensei_delete_quiz_grade( $quiz_id = 0, $user_id = 0 ): bool {
if ( intval( $user_id ) === 0 ) {
$user_id = get_current_user_id();
}
if ( ! $quiz_id || ! $user_id ) {
return false;
}
$quiz_submission = Sensei()->quiz_submission_repository->get( $quiz_id, $user_id );
if ( ! $quiz_submission ) {
return false;
}
$quiz_submission->set_final_grade( null );
Sensei()->quiz_submission_repository->save( $quiz_submission );
return true;
}
/**
* Add answer notes to question
*
* @deprecated 4.19.2
*
* @param integer $question_id ID of question
* @param integer $user_id ID of user
* @param string $notes
* @return boolean
*/
public static function sensei_add_answer_notes( $question_id = 0, $user_id = 0, $notes = '' ) {
_deprecated_function( __METHOD__, '4.19.2', 'Sensei_Quiz::save_user_answers_feedback' );
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
$activity_logged = false;
if ( intval( $question_id ) > 0 ) {
$notes = base64_encode( $notes );
// Don't store empty values, no point
if ( ! empty( $notes ) ) {
$user_lesson_id = self::sensei_get_activity_value(
array(
'post_id' => $question_id,
'user_id' => $user_id,
'type' => 'sensei_user_answer',
'field' => 'comment_ID',
)
);
$activity_logged = update_comment_meta( $user_lesson_id, 'answer_note', $notes );
} else {
$activity_logged = true;
}
}
return $activity_logged;
}
/**
* array_sort_reorder handle sorting of table data
*
* @since 1.3.0
* @param array $return_array data to be ordered
* @return array $return_array ordered data
*/
public static function array_sort_reorder( $return_array ) {
if ( isset( $_GET['orderby'] ) && '' != esc_html( $_GET['orderby'] ) ) {
$sort_key = '';
if ( '' != $sort_key ) {
self::sort_array_by_key( $return_array, $sort_key );
if ( isset( $_GET['order'] ) && 'desc' == esc_html( $_GET['order'] ) ) {
$return_array = array_reverse( $return_array, true );
}
}
return $return_array;
} else {
return $return_array;
}
}
/**
* sort_array_by_key sorts array by key
*
* @since 1.3.0
* @param array $array by ref
* @param $key string column name in array
* @return void
*/
public static function sort_array_by_key( $array, $key ) {
$sorter = array();
$ret = array();
reset( $array );
foreach ( $array as $ii => $va ) {
$sorter[ $ii ] = $va[ $key ];
}
asort( $sorter );
foreach ( $sorter as $ii => $va ) {
$ret[ $ii ] = $array[ $ii ];
}
$array = $ret;
}
/**
* This function returns an array of lesson quiz questions
*
* @since 1.3.2
* @since 3.5.0 Added $query_args.
*
* @param integer $quiz_id
* @param array $query_args Additional args for the query.
* @return array of quiz questions
*/
public static function lesson_quiz_questions( $quiz_id = 0, $query_args = [] ) {
$questions_array = array();
if ( 0 < $quiz_id ) {
$defaults = array(
'post_type' => 'question',
'posts_per_page' => -1,
'orderby' => 'ID',
'order' => 'ASC',
'meta_query' => array(
array(
'key' => '_quiz_id',
'value' => $quiz_id,
),
),
'post_status' => 'any',
'suppress_filters' => 0,
);
$question_args = wp_parse_args( $query_args, $defaults );
$questions_array = get_posts( $question_args );
}
return $questions_array;
}
/**
* Complete this course forcefully for this user by passing all the lessons.
*
* @param int $user_id User ID.
* @param int $course_id Course ID
*/
public static function force_complete_user_course( $user_id, $course_id ) {
$user = get_user_by( 'id', $user_id );
if ( false === $user ) {
return;
}
$lesson_ids = Sensei()->course->course_lessons( $course_id, 'any', 'ids' );
foreach ( $lesson_ids as $id ) {
self::sensei_start_lesson( $id, $user_id, true );
}
}
/**
* Get pass mark for course
*
* @param integer $course_id ID of course
* @return integer Pass mark for course
*/
public static function sensei_course_pass_grade( $course_id = 0 ) {
$course_passmark = 0;
if ( $course_id > 0 ) {
$lessons = Sensei()->course->course_lessons( $course_id );
$lesson_count = 0;
$total_passmark = 0;
foreach ( $lessons as $lesson ) {
// Get Quiz ID
$quiz_id = Sensei()->lesson->lesson_quizzes( $lesson->ID );
// Check for a pass being required
$pass_required = get_post_meta( $quiz_id, '_pass_required', true );
if ( $pass_required ) {
// Get quiz passmark
$quiz_passmark = absint( get_post_meta( $quiz_id, '_quiz_passmark', true ) );
// Add up total passmark
$total_passmark += $quiz_passmark;
++$lesson_count;
}
}
// Might be a case of no required lessons
if ( $lesson_count ) {
$course_passmark = ( $total_passmark / $lesson_count );
}
}
/**
* Filter the course pass mark
*
* @since 1.9.7
*
* @hook sensei_course_pass_grade
*
* @param {int} $course_passmark Pass mark for course.
* @param {int} $course_id ID of course.
* @return {int} Filtered course pass mark.
*/
return apply_filters( 'sensei_course_pass_grade', self::round( $course_passmark ), $course_id );
}
/**
* Get user total grade for course
*
* @param int $course_id ID of course
* @param int $user_id ID of user
* @return int User's total grade
*/
public static function sensei_course_user_grade( $course_id = 0, $user_id = 0 ) {
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
$total_grade = 0;
if ( $course_id > 0 && $user_id > 0 ) {
$lessons = Sensei()->course->course_lessons( $course_id );
$lesson_count = 0;
$total_grade = 0;
foreach ( $lessons as $lesson ) {
// Check for lesson having questions, thus a quiz, thus having a grade
$has_questions = Sensei()->lesson->lesson_has_quiz_with_graded_questions( $lesson->ID );
if ( $has_questions ) {
$quiz_id = Sensei()->lesson->lesson_quizzes( $lesson->ID );
$user_has_quiz_progress = Sensei()->quiz_progress_repository->has( $quiz_id, $user_id );
if ( ! $user_has_quiz_progress ) {
continue;
}
// Get user quiz grade
$submission = Sensei()->quiz_submission_repository->get( $quiz_id, $user_id );
$quiz_grade = $submission ? $submission->get_final_grade() : 0;
// Add up total grade
$total_grade += intval( $quiz_grade );
++$lesson_count;
}
}
// Might be a case of no lessons with quizzes
if ( $lesson_count ) {
$total_grade = ( $total_grade / $lesson_count );
}
}
/**
* Filter the user total grade for course
*
* @since 1.9.7
*
* @hook sensei_course_user_grade
*
* @param {int} $total_grade User's total grade
* @param {int} $course_id ID of course
* @param {int} $user_id ID of user
* @return {int} Filtered user total grade.
*/
return apply_filters( 'sensei_course_user_grade', self::round( $total_grade ), $course_id, $user_id );
}
/**
* Check if user has passed a course
*
* @param int $course_id ID of course
* @param int $user_id ID of user
* @return bool
*/
public static function sensei_user_passed_course( $course_id = 0, $user_id = 0 ) {
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
$pass = false;
if ( $course_id > 0 && $user_id > 0 ) {
$passmark = self::sensei_course_pass_grade( $course_id );
$user_grade = self::sensei_course_user_grade( $course_id, $user_id );
if ( $user_grade >= $passmark ) {
$pass = true;
}
}
return $pass; // Should add the $passmark and $user_grade as part of the return!
}
/**
* Set the status message displayed to the user for a course
*
* @param integer $course_id ID of course
* @param integer $user_id ID of user
*/
public static function sensei_user_course_status_message( $course_id = 0, $user_id = 0 ) {
if ( intval( $user_id ) == 0 ) {
$user_id = get_current_user_id();
}
$status = 'not_started';
$box_class = 'info';
$message = __( 'You have not started this course yet.', 'sensei-lms' );
if ( $course_id > 0 && $user_id > 0 ) {
$started_course = self::has_started_course( $course_id, $user_id );
if ( $started_course ) {
$passmark = self::sensei_course_pass_grade( $course_id ); // This happens inside sensei_user_passed_course()!
$user_grade = self::sensei_course_user_grade( $course_id, $user_id ); // This happens inside sensei_user_passed_course()!
// if the user has started the course but there is no passmark
// then do not show passed/failed messages.
if ( ! $passmark ) {
return;
}
if ( $user_grade >= $passmark ) {
$status = 'passed';
$box_class = 'tick';
// translators: Placeholder is the user's grade.
$message = sprintf( __( 'You have passed this course with a grade of %1$d%%.', 'sensei-lms' ), $user_grade );
} else {
$status = 'failed';
$box_class = 'alert';
// translators: Placeholders are the required grade and the actual grade, respectively.
$message = sprintf( __( 'You require %1$d%% to pass this course. Your grade is %2$s%%.', 'sensei-lms' ), $passmark, $user_grade );
}
}
}
/**
* Filter a message for user course status.
*
* Possible statuses: not_started, passed, failed.
*
* @hook sensei_user_course_status_{status}
*
* @param {string} $message Status message.
* @return {string} Filtered status message.
*/
$message = apply_filters( 'sensei_user_course_status_' . $status, $message );
Sensei()->notices->add_notice( $message, $box_class );
}
/**
* Set the status message displayed to the user for a quiz
*
* @param integer $lesson_id ID of quiz lesson
* @param integer $user_id ID of user
* @param bool $is_lesson
* @return array Status code and message
*/
public static function sensei_user_quiz_status_message( $lesson_id = 0, $user_id = 0, $is_lesson = false ) {
global $current_user;
if ( intval( $user_id ) == 0 ) {
$user_id = $current_user->ID;
}
$status = 'not_started';
$box_class = 'info';
$message = __( "You have not taken this lesson's quiz yet", 'sensei-lms' );
$extra = '';
if ( $lesson_id > 0 && $user_id > 0 ) {
// Course ID.
$course_id = absint( get_post_meta( $lesson_id, '_lesson_course', true ) );
// Has user started course.
$started_course = Sensei_Course::is_user_enrolled( $course_id, $user_id );
// Has user completed lesson.
$lesson_complete = self::user_completed_lesson( $lesson_id, $user_id );
// Quiz ID.
$quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id );
// Quiz progress.
$user_quiz_progress = Sensei()->quiz_progress_repository->get( $quiz_id, $user_id );
// Quiz grade.
$submission = Sensei()->quiz_submission_repository->get( $quiz_id, $user_id );
$quiz_grade = $submission ? $submission->get_final_grade() : 0;
// Quiz passmark.
$quiz_passmark = absint( get_post_meta( $quiz_id, '_quiz_passmark', true ) );
// Pass required.
$pass_required = get_post_meta( $quiz_id, '_pass_required', true );
// Quiz questions.
$has_quiz_questions = Sensei_Lesson::lesson_quiz_has_questions( $lesson_id );
if ( ! $started_course ) {
$status = 'not_started_course';
$box_class = 'info';
// translators: Placeholders are an opening and closing <a> tag linking to the course permalink.
$message = sprintf( __( 'Please sign up for %1$sthe course%2$s before taking this quiz', 'sensei-lms' ), '<a href="' . esc_url( get_permalink( $course_id ) ) . '" title="' . esc_attr( __( 'Sign Up', 'sensei-lms' ) ) . '">', '</a>' );
} elseif ( ! is_user_logged_in() ) {
$status = 'login_required';
$box_class = 'info';
$message = __( 'You must be logged in to take this quiz', 'sensei-lms' );
}
// Lesson/Quiz is marked as complete thus passing any quiz restrictions
elseif ( $lesson_complete ) {
$status = 'passed';
$box_class = 'tick';
// Lesson status will be "complete" (has no Quiz)
if ( ! $has_quiz_questions ) {
$message = sprintf( __( 'Congratulations! You have passed this lesson.', 'sensei-lms' ) );
}
// Lesson status will be "graded" (no passmark required so might have failed all the questions)
elseif ( empty( $quiz_grade ) ) {
$message = sprintf( __( 'Congratulations! You have completed this lesson.', 'sensei-lms' ) );
}
// Lesson status will be "passed" (passmark reached)
elseif ( ! empty( $quiz_grade ) && abs( $quiz_grade ) >= 0 ) {
if ( $is_lesson ) {
// translators: Placeholder is the quiz grade.
$message = sprintf( __( 'Congratulations! You have passed this lesson\'s quiz achieving %s%%', 'sensei-lms' ), self::round( $quiz_grade, 2 ) );
} else {
// translators: Placeholder is the quiz grade.
$message = sprintf( __( 'Congratulations! You have passed this quiz achieving %s%%', 'sensei-lms' ), self::round( $quiz_grade, 2 ) );
}
}
// add next lesson button
$nav_links = sensei_get_prev_next_lessons( $lesson_id );
// Output HTML
if ( isset( $nav_links['next'] ) ) {
if ( ! $is_lesson || ! has_block( 'sensei-lms/lesson-actions', $lesson_id ) ) {
$message .= ' <a class="next-lesson" href="' . esc_url( $nav_links['next']['url'] )
. '" rel="next"><span class="meta-nav"></span>' . __( 'Next Lesson', 'sensei-lms' )
. '</a>';
}
}
} else { // Lesson/Quiz not complete.
$lesson_prerequisite = \Sensei_Lesson::find_first_prerequisite_lesson( $lesson_id, $user_id );
if ( ! $is_lesson && $lesson_prerequisite > 0 ) {
$prerequisite_quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_prerequisite );
if ( $prerequisite_quiz_id ) {
// If there is a quiz for the prerequisite lesson, use the quiz progress.
$prerequisite_progress = Sensei()->quiz_progress_repository->get( $prerequisite_quiz_id, $user_id );
} else {
// If there is no quiz for the prerequisite lesson, use the lesson progress.
$prerequisite_progress = Sensei()->lesson_progress_repository->get( $lesson_prerequisite, $user_id );
}
$prerequisite_lesson_link = '<a href="'
. esc_url( get_permalink( $lesson_prerequisite ) )
. '" title="'
// translators: Placeholder is the item title.
. sprintf( esc_attr__( 'You must first complete: %1$s', 'sensei-lms' ), get_the_title( $lesson_prerequisite ) )
. '">'
. esc_html__( 'prerequisites', 'sensei-lms' )
. '</a>';
$message = ! empty( $prerequisite_progress ) && 'ungraded' === $prerequisite_progress->get_status()
// translators: Placeholder is the link to the prerequisite lesson.
? sprintf( esc_html__( 'You will be able to access this quiz once the %1$s are completed and graded.', 'sensei-lms' ), $prerequisite_lesson_link )
// translators: Placeholder is the link to the prerequisite lesson.
: sprintf( esc_html__( 'Please complete the %1$s to access this quiz.', 'sensei-lms' ), $prerequisite_lesson_link );
// Lesson/Quiz isn't "complete" instead it's ungraded (previously this "state" meant that it *was* complete).
} elseif ( $user_quiz_progress && 'ungraded' == $user_quiz_progress->get_status() ) {
$status = 'complete';
$box_class = 'info';
if ( $is_lesson ) {
// translators: Placeholders are an opening and closing <a> tag linking to the quiz permalink.
$message = sprintf( __( 'You have completed this lesson\'s quiz and it will be graded soon. %1$sView the lesson quiz%2$s', 'sensei-lms' ), '<a href="' . esc_url( get_permalink( $quiz_id ) ) . '" title="' . esc_attr( get_the_title( $quiz_id ) ) . '">', '</a>' );
} else {
// translators: Placeholder is the quiz passmark.
$message = sprintf( __( 'You have completed this quiz and it will be graded soon. You require %1$s%% to pass.', 'sensei-lms' ), self::round( $quiz_passmark, 2 ) );
}
// Lesson status must be "failed".
} elseif ( $user_quiz_progress && 'failed' == $user_quiz_progress->get_status() ) {
$status = 'failed';
$box_class = 'alert';
if ( $is_lesson ) {
// translators: Placeholders are the quiz passmark and the learner's grade, respectively.
$message = sprintf( __( 'You require %1$d%% to pass this lesson\'s quiz. Your grade is %2$s%%', 'sensei-lms' ), self::round( $quiz_passmark, 2 ), self::round( $quiz_grade, 2 ) );
} else {
// translators: Placeholders are the quiz passmark and the learner's grade, respectively.
$message = sprintf( __( 'You require %1$d%% to pass this quiz. Your grade is %2$s%%', 'sensei-lms' ), self::round( $quiz_passmark, 2 ), self::round( $quiz_grade, 2 ) );
}
// Lesson/Quiz requires a pass.
} elseif ( $pass_required ) {
$status = 'not_started';
$box_class = 'info';
if ( ! Sensei_Lesson::is_prerequisite_complete( $lesson_id, get_current_user_id() ) ) {
$message = '';
} elseif ( $is_lesson ) {
// translators: Placeholder is the quiz passmark.
$message = sprintf( __( 'You require %1$d%% to pass this lesson\'s quiz.', 'sensei-lms' ), self::round( $quiz_passmark, 2 ) );
} else {
// translators: Placeholder is the quiz passmark.
$message = sprintf( __( 'You require %1$d%% to pass this quiz.', 'sensei-lms' ), self::round( $quiz_passmark, 2 ) );
}
}
}
} else {
$course_id = Sensei()->lesson->get_course_id( $lesson_id );
$course_link = '<a href="' . esc_url( get_permalink( $course_id ) ) . '" title="' . esc_attr__( 'Sign Up', 'sensei-lms' ) . '">';
$course_link .= esc_html__( 'course', 'sensei-lms' );
$course_link .= '</a>';
// translators: Placeholder is a link to the course permalink.
$message_default = sprintf( __( 'Please sign up for the %1$s before taking this quiz.', 'sensei-lms' ), $course_link );
/**
* Filter the course sign up notice message on the quiz page.
*
* @since 2.0.0
*
* @hook sensei_quiz_course_signup_notice_message
*
* @param {string} $message Message to show user.
* @param {int} $course_id Post ID for the course.
* @param {string} $course_link Generated HTML link to the course.
* @return {string} Filtered message.
*/
$message = apply_filters( 'sensei_quiz_course_signup_notice_message', $message_default, $course_id, $course_link );
}
/**
* Filter a message for user quiz status. Legacy filter.
*
* Possible statuses: not_started, passed, failed.
*
* @hook sensei_user_quiz_status_{status}
*
* @param {string} $message Status message.
* @return {string} Filtered status message.
*/
$message = apply_filters( 'sensei_user_quiz_status_' . $status, $message );
if ( $is_lesson && ! in_array( $status, array( 'login_required', 'not_started_course' ) ) ) {
$quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id );
$extra = '<p><a class="button" href="' . esc_url( get_permalink( $quiz_id ) ) . '" title="' . __( 'View the lesson quiz', 'sensei-lms' ) . '">' . __( 'View the lesson quiz', 'sensei-lms' ) . '</a></p>';
}
/**
* Filter user quiz status.
*
* @hook sensei_user_quiz_status
*
* @param {array} $status_data Array containing the status, message and additions information.
* @param {int} $lesson_id Lesson ID.
* @param {int} $user_id User ID.
* @param {bool} $is_leeson A flag is $lesson_id is a lesson.
* @return {array} Filtered quiz status.
*/
return apply_filters(
'sensei_user_quiz_status',
array(
'status' => $status,
'box_class' => $box_class,
'message' => $message,
'extra' => $extra,
),
$lesson_id,
$user_id,
$is_lesson
);
}
/**
* Start course for user
*
* @since 1.4.8
* @param integer $user_id User ID
* @param integer $course_id Course ID
* @return bool|int False if they haven't started; Comment ID of course progress if they have.
*/
public static function user_start_course( $user_id = 0, $course_id = 0 ) {
$activity_comment_id = false;
if ( $user_id && $course_id ) {
// Check if user is already on the Course.
$activity_comment_id = self::get_course_progress_comment_id( $course_id, $user_id );
if ( false === $activity_comment_id ) {
$activity_comment_id = self::start_user_on_course( $user_id, $course_id );
}
}
return $activity_comment_id;
}
/**
* Check if a user has started a course or not.
*
* @since 1.7.0
* @deprecated 3.0.0 No longer returns comment ID when they have access. To check if a user is enrolled use `Sensei_Course::is_user_enrolled()`. For course progress check, use `Sensei_Utils::has_started_course()`.
*
* @param int $course_id Course ID.
* @param int $user_id User ID.
* @return bool
*/
public static function user_started_course( $course_id = 0, $user_id = 0 ) {
_deprecated_function( __METHOD__, '3.0.0', '`Sensei_Course::is_user_enrolled()`. For course progress check, use `Sensei_Utils::has_started_course()`' );
if ( empty( $course_id ) ) {
return false;
}
// This was mainly used to check if a user was enrolled in a course. For now, use this replacement method.
return Sensei_Course::is_user_enrolled( $course_id, $user_id );
}
/**
* Get the course progress comment ID, if it exists.
*
* @since 3.0.0
*
* @param int $course_id Course ID.
* @param int $user_id User ID.
* @return int|false false or comment_ID
*/
public static function get_course_progress_comment_id( $course_id, $user_id = null ) {
if ( empty( $course_id ) ) {
return false;
}
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
if ( ! $user_id ) {
return false;
}
$course_progress = Sensei()->course_progress_repository->get( $course_id, $user_id );
if ( ! $course_progress ) {
return false;
}
return $course_progress->get_id();
}
/**
* Check if a user has started a course or not.
*
* @since 3.0.0
*
* @param int $course_id Course ID.
* @param int $user_id User ID.
* @return int|bool false or comment_ID
*/
public static function has_started_course( $course_id = 0, $user_id = 0 ) {
$user_started_course = self::get_course_progress_comment_id( $course_id, $user_id );
/**
* Filter the user started course value
*
* @since 1.9.3
*
* @hook sensei_user_started_course
*
* @param {bool|int} $user_started_course False if the user has not started the course, otherwise the comment ID of the course progress.
* @param {int} $course_id The course ID.
* @param {int} $user_id The user ID.
* @return {bool|int} Filtered user started course ID.
*/
return apply_filters( 'sensei_user_started_course', $user_started_course, $course_id, $user_id );
}
/**
* Checks if a user has completed a course by checking every lesson status,
* and then updates the course metadata with that information.
*
* @since 1.7.0
* @param integer $course_id Course ID
* @param integer $user_id User ID
* @return mixed boolean or comment_ID
*/
public static function user_complete_course( $course_id = 0, $user_id = 0, $trigger_completion_action = true ) {
if ( ! $course_id ) {
return false;
}
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
$course_progress = Sensei()->course_progress_repository->get( $course_id, $user_id );
if ( ! $course_progress ) {
$course_progress = Sensei()->course_progress_repository->create( $course_id, $user_id );
}
if ( ! $course_progress->get_started_at() ) {
$course_progress->start();
}
$lessons_completed = 0;
// Grab all of this Courses' lessons, looping through each...
$lesson_ids = Sensei()->course->course_lessons( $course_id, 'publish', 'ids' );
$total_lessons = count( $lesson_ids );
// ...if course completion not set to 'passed', and all lessons are complete or graded,
// ......then all lessons are 'passed'
// ...else if course completion is set to 'passed', check if each lesson has questions...
// ......if no questions yet the status is 'complete'
// .........then the lesson is 'passed'
// ......else if questions check the lesson status has a grade and that the grade is greater than the lesson passmark
// .........then the lesson is 'passed'
// ...if all lessons 'passed' then update the course status to complete
// The below checks if a lesson is fully completed, though maybe should be Utils::user_completed_lesson()
$lesson_progress_args = array(
'user_id' => $user_id,
'lesson_id' => $lesson_ids,
);
$all_lesson_progress = Sensei()->lesson_progress_repository->find( $lesson_progress_args );
foreach ( $all_lesson_progress as $lesson_progress ) {
if ( $lesson_progress->is_complete() ) {
$lessons_completed++;
}
}
if ( $lessons_completed === $total_lessons ) {
$course_progress->complete();
}
Sensei()->course_progress_repository->save( $course_progress );
$course_progress_metadata = [
// How many lessons have been completed.
'complete' => $lessons_completed,
// Overall percentage of the course lessons complete (or graded) compared to 'in-progress' regardless of the above.
'percent' => self::quotient_as_absolute_rounded_percentage( $lessons_completed, $total_lessons ),
];
foreach ( $course_progress_metadata as $key => $value ) {
update_comment_meta( $course_progress->get_id(), $key, $value );
}
// Allow further actions.
if ( 'complete' === $course_progress->get_status() && true === $trigger_completion_action ) {
/**
* Fires when a user completes a course.
*
* @hook sensei_user_course_end
*
* @param {int} $user_id User ID.
* @param {int} $course_id Course ID.
*/
do_action( 'sensei_user_course_end', $user_id, $course_id );
}
return $course_progress->get_id();
}
/**
* Get completion percentage.
*
* @param mixed $numerator Numerator.
* @param int $denominator Denominator.
* @param int $decimal_places_to_round Decimal places to round.
* @return float
*/
public static function quotient_as_absolute_rounded_percentage( $numerator, $denominator, $decimal_places_to_round = 0 ) {
return self::quotient_as_absolute_rounded_number( $numerator * 100.0, $denominator, $decimal_places_to_round );
}
/**
* Get formatted quotient.
*
* @param mixed $numerator Numerator.
* @param int $denominator Denominator.
* @param int $decimal_places_to_round Decimal places to round.
* @return float
*/
public static function quotient_as_absolute_rounded_number( $numerator, $denominator, $decimal_places_to_round = 0 ) {
if ( 0 === $denominator ) {
return 0;
}
return self::as_absolute_rounded_number( floatval( $numerator ) / ( $denominator ), $decimal_places_to_round );
}
/**
* Round a number to a given number of decimal places.
*
* @param mixed $number Number to round.
* @param int $decimal_places_to_round Decimal places to round.
* @return float
*/
public static function as_absolute_rounded_number( $number, $decimal_places_to_round = 0 ) {
return abs( round( ( floatval( $number ) ), $decimal_places_to_round ) );
}
/**
* Check if a user has completed a course or not
*
* @param int | WP_Post | WP_Comment $course course_id or sensei_course_status entry
* @param int $user_id User ID.
* @return boolean
*/
public static function user_completed_course( $course, $user_id = 0 ) {
if ( ! $course ) {
return false;
}
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
if ( empty( $user_id ) ) {
return false;
}
$user_course_status = null;
if ( is_object( $course ) && is_a( $course, 'WP_Comment' ) ) {
$user_course_status = $course->comment_approved;
} elseif ( ! is_numeric( $course ) && ! is_a( $course, 'WP_Post' ) ) {
$user_course_status = $course;
} else {
if ( is_a( $course, 'WP_Post' ) ) {
$course = $course->ID;
} else {
$course = (int) $course;
}
$course_progress = Sensei()->course_progress_repository->get( $course, $user_id );
if ( $course_progress ) {
$user_course_status = $course_progress->get_status();
}
}
if ( $user_course_status && Course_Progress_Interface::STATUS_COMPLETE === $user_course_status ) {
return true;
}
return false;
}
/**
* Check if a user has started a lesson or not
*
* @since 1.7.0
* @param int $lesson_id
* @param int $user_id
* @return mixed false or comment_ID
*/
public static function user_started_lesson( $lesson_id = 0, $user_id = 0 ) {
if ( ! $lesson_id ) {
return false;
}
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
$lesson_progress = Sensei()->lesson_progress_repository->get( $lesson_id, $user_id );
if ( ! $lesson_progress ) {
return false;
}
return $lesson_progress->get_id();
}
/**
* Get the number of lessons of a course that a user started.
*
* @since 3.13.3
*
* @param int $course_id The course id.
* @param int $user_id The user id.
*
* @return int Lesson count.
*/
public static function user_started_lesson_count( int $course_id, int $user_id ) : int {
return Sensei()->lesson_progress_repository->count( $course_id, $user_id );
}
/**
* Check if a user has completed a lesson or not
*
* @uses Sensei()
* @param int|WP_Comment|string $lesson lesson id (int), lesson status (string), or sensei_lesson_status entry (WP_Comment).
* @param int $user_id User ID.
* @return boolean
*/
public static function user_completed_lesson( $lesson = 0, $user_id = 0 ): bool {
if ( ! $lesson ) {
return false;
}
$lesson_id = 0;
if ( is_object( $lesson ) ) {
$user_lesson_status = $lesson->comment_approved;
$lesson_id = $lesson->comment_post_ID;
} elseif ( ! is_numeric( $lesson ) ) {
$user_lesson_status = $lesson;
} else {
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
// the user is not logged in.
if ( 0 >= (int) $user_id ) {
return false;
}
$lesson_id = (int) $lesson;
$user_id = (int) $user_id;
/**
* Filter lesson ID for `Sensei_Utils::user_completed_lesson` method.
*
* @hook sensei_utils_user_completed_lesson_lesson_id
*
* @param {int} $lesson_id ID of lesson.
* @return {int} Filtered lesson ID.
*/
$lesson_id = apply_filters( 'sensei_utils_user_completed_lesson_lesson_id', $lesson_id );
$lesson_progress = Sensei()->lesson_progress_repository->get( $lesson_id, $user_id );
if ( $lesson_progress ) {
$user_lesson_status = $lesson_progress->get_status();
} else {
return false; // No progress means not complete.
}
// In the comments-based progress we use one entry to store both the lesson progress and the quiz progress.
// In the tables-based progress we split them. Here is important to use the quiz proress if the quiz pass is required.
$lesson_quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id );
if ( $lesson_quiz_id ) {
$pass_required = get_post_meta( $lesson_quiz_id, '_pass_required', true );
if ( $pass_required ) {
$quiz_progress = Sensei()->quiz_progress_repository->get( $lesson_quiz_id, $user_id );
if ( $quiz_progress ) {
$user_lesson_status = $quiz_progress->get_status();
} else {
return false;
}
}
}
}
/**
* Filter the user lesson status
*
* @since 1.9.7
*
* @hook sensei_user_completed_lesson
*
* @param {string} $user_lesson_status User lesson status.
* @param {int} $lesson_id ID of lesson.
* @param {int} $user_id ID of user.
* @return {string} Filtered user lesson status.
*/
$user_lesson_status = apply_filters( 'sensei_user_completed_lesson', $user_lesson_status, $lesson_id, $user_id );
if ( 'in-progress' === $user_lesson_status ) {
return false;
}
// Check for Passed or Completed Setting
// Should we be checking for the Course completion setting?
// Surely that should only affect the Course completion, not bypass each Lesson setting.
switch ( $user_lesson_status ) {
case 'complete':
case 'graded':
case 'passed':
return true;
case 'failed':
// This may be 'completed' depending on...
if ( $lesson_id ) {
// Get Quiz ID, this won't be needed once all Quiz meta fields are stored on the Lesson.
$lesson_quiz_id = Sensei()->lesson->lesson_quizzes( $lesson_id );
if ( $lesson_quiz_id ) {
// ...the quiz pass setting
$pass_required = get_post_meta( $lesson_quiz_id, '_pass_required', true );
if ( empty( $pass_required ) ) {
// We just require the user to have done the quiz, not to have passed.
return true;
}
}
}
return false;
}
return false;
}
/**
* Returns the requested course status
*
* @since 1.7.0
* @deprecated 4.18.0 Use course progress repository instead.
*
* @param int $course_id Course ID.
* @param int $user_id User ID.
* @return object
*/
public static function user_course_status( $course_id = 0, $user_id = 0 ) {
if ( $course_id ) {
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
$user_course_status = self::sensei_check_for_activity(
array(
'post_id' => $course_id,
'user_id' => $user_id,
'type' => 'sensei_course_status',
),
true
);
return $user_course_status;
}
return false;
}
/**
* Returns the requested lesson status
*
* @since 1.7.0
* @param int $lesson_id Lesson ID.
* @param int $user_id User ID.
* @return WP_Comment|false
*/
public static function user_lesson_status( $lesson_id = 0, $user_id = 0 ) {
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
if ( $lesson_id > 0 && $user_id > 0 ) {
$user_lesson_status = self::sensei_check_for_activity(
array(
'post_id' => $lesson_id,
'user_id' => $user_id,
'type' => 'sensei_lesson_status',
),
true
);
return $user_lesson_status;
}
return false;
}
/**
* Returns if a lesson is a preview lesson or not.
*
* @param int $lesson_id Lesson ID.
* @return bool
*/
public static function is_preview_lesson( $lesson_id ) {
$is_preview = false;
if ( 'lesson' == get_post_type( $lesson_id ) ) {
$lesson_preview = get_post_meta( $lesson_id, '_lesson_preview', true );
if ( isset( $lesson_preview ) && '' != $lesson_preview ) {
$is_preview = true;
}
}
return $is_preview;
}
/**
* Returns if a user has passed a quiz or not.
*
* @param int $quiz_id Quiz ID.
* @param int $user_id User ID.
* @return bool
*/
public static function user_passed_quiz( $quiz_id = 0, $user_id = 0 ) {
if ( ! $quiz_id ) {
return false;
}
if ( ! $user_id ) {
$user_id = get_current_user_id();
}
// Quiz Grade
$submission = \Sensei()->quiz_submission_repository->get( $quiz_id, $user_id );
$quiz_grade = $submission ? $submission->get_final_grade() : 0;
// Check if Grade is greater than or equal to pass percentage
$quiz_passmark = self::as_absolute_rounded_number( get_post_meta( $quiz_id, '_quiz_passmark', true ), 2 );
if ( $quiz_passmark <= intval( $quiz_grade ) ) {
return true;
}
return false;
}
/**
* Sets the status for the lesson
*
* @since 1.7.0
*
* @param int|string $user_id
* @param int|string $lesson_id
* @param string $status
* @param array $metadata
*
* @return mixed false or comment_ID
*/
public static function update_lesson_status( $user_id, $lesson_id, $status = 'in-progress', $metadata = array() ) {
$comment_id = false;
if ( ! empty( $status ) ) {
$args = array(
'user_id' => $user_id,
'username' => get_userdata( $user_id )->user_login ?? null,
'post_id' => $lesson_id,
'status' => $status,
'type' => 'sensei_lesson_status', /* FIELD SIZE 20 */
'action' => 'update', // Update the existing status...
'keep_time' => true, // ...but don't change the existing timestamp
);
if ( in_array( $status, array( 'in-progress', 'ungraded', 'graded', 'passed', 'failed' ), true ) ) {
unset( $args['keep_time'] ); // Keep updating what's happened
}
$comment_id = self::sensei_log_activity( $args );
if ( $comment_id && ! empty( $metadata ) ) {
foreach ( $metadata as $key => $value ) {
update_comment_meta( $comment_id, $key, $value );
}
}
/**
* Fires when a lesson status is updated.
*
* @hook sensei_lesson_status_updated
*
* @param {string} $status The status.
* @param {int} $user_id The user ID.
* @param {int} $lesson_id The lesson ID.
* @param {int} $comment_id The status comment ID.
*/
do_action( 'sensei_lesson_status_updated', $status, $user_id, $lesson_id, $comment_id );
}
return $comment_id;
}
/**
* Sets the statuses for the Course
*
* @access public
* @since 1.7.0
* @param int $user_id
* @param int $course_id
* @param string $status
* @param array $metadata
* @return mixed false or comment_ID
*/
public static function update_course_status( $user_id, $course_id, $status = 'in-progress', $metadata = array() ) {
$comment_id = false;
if ( ! empty( $status ) ) {
$course_progress = Sensei()->course_progress_repository->get( $course_id, $user_id );
$previous_status = $course_progress ? $course_progress->get_status() : null;
$args = array(
'user_id' => $user_id,
'username' => get_userdata( $user_id )->user_login ?? null,
'post_id' => $course_id,
'status' => $status,
'type' => 'sensei_course_status', /* FIELD SIZE 20 */
'action' => 'update', // Update the existing status...
);
$comment_id = self::sensei_log_activity( $args );
if ( $comment_id && ! empty( $metadata ) ) {
foreach ( $metadata as $key => $value ) {
update_comment_meta( $comment_id, $key, $value );
}
}
/**
* Fires when a course status is updated.
*
* @since 1.7.0
* @since 4.20.1 $previous_status parameter added.
*
* @hook sensei_course_status_updated
*
* @param {string} $status The status.
* @param {int} $user_id The user ID.
* @param {int} $course_id The course ID.
* @param {int} $comment_id The comment ID.
* @param {string|null} $previous_status The previous status. Null if previous status was not set.
*/
do_action( 'sensei_course_status_updated', $status, $user_id, $course_id, $comment_id, $previous_status );
}
return $comment_id;
}
/**
* Remove the orderby for comments
*
* @access public
* @since 1.7.0
* @param array $pieces (default: array())
* @return array
*/
public static function single_comment_filter( $pieces ) {
unset( $pieces['orderby'] );
unset( $pieces['order'] );
return $pieces;
}
/**
* Allow retrieving comments with any comment_approved status, little bypass to WP_Comment. Required only for WP < 4.1
*
* @access public
*
* @deprecated 3.13.4
*
* @since 1.7.0
* @param array $pieces (default: array())
* @return array
*/
public static function comment_any_status_filter( $pieces ) {
_deprecated_function( __FUNCTION__, '3.13.4' );
$pieces['where'] = str_replace( array( "( comment_approved = '0' OR comment_approved = '1' ) AND", "comment_approved = 'any' AND" ), '', $pieces['where'] );
return $pieces;
}
/**
* Allow retrieving comments within multiple statuses, little bypass to WP_Comment. Required only for WP < 4.1
*
* @access public
*
* @deprecated 3.13.4
*
* @since 1.7.0
* @param array $pieces (default: array())
* @return array
*/
public static function comment_multiple_status_filter( $pieces ) {
_deprecated_function( __FUNCTION__, '3.13.4' );
preg_match( "/^comment_approved = '([a-z\-\,]+)'/", $pieces['where'], $placeholder );
if ( ! empty( $placeholder[1] ) ) {
$statuses = explode( ',', $placeholder[1] );
$pieces['where'] = str_replace( "comment_approved = '" . $placeholder[1] . "'", "comment_approved IN ('" . implode( "', '", $statuses ) . "')", $pieces['where'] );
}
return $pieces;
}
/**
* Adjust the comment query to be faster on the database, used by Analysis admin
*
* @since 1.7.0
* @param array $pieces
* @return array $pieces
*/
public static function comment_total_sum_meta_value_filter( $pieces ) {
global $wpdb;
$pieces['fields'] = " COUNT(*) AS total, SUM($wpdb->commentmeta.meta_value) AS meta_sum ";
unset( $pieces['groupby'] );
return $pieces;
}
/**
* Shifts counting of posts to the database where it should be. Likely not to be used due to knock on issues.
*
* @access public
* @since 1.7.0
* @param array $pieces (default: array())
* @return array
*/
public static function get_posts_count_only_filter( $pieces ) {
$pieces['fields'] = ' COUNT(*) AS total ';
unset( $pieces['groupby'] );
return $pieces;
}
/**
*
* Alias to Woothemes_Sensei_Utils::update_user_data
*
* @since 1.7.4
*
* @param string $data_key maximum 39 characters allowed
* @param int $post_id
* @param mixed $value
* @param int $user_id
*
* @return bool $success
*/
public static function add_user_data( $data_key, $post_id, $value = '', $user_id = 0 ) {
return self::update_user_data( $data_key, $post_id, $value, $user_id );
}
/**
* add user specific data to the passed in sensei post type id
*
* This function saves comment meta on the users current status. If no status is available
* status will be created. It only operates on the available sensei Post types: course, lesson, quiz.
*
* @since 1.7.4
*
* @param string $data_key maximum 39 characters allowed
* @param int $post_id
* @param mixed $value
* @param int $user_id
*
* @return bool $success
*/
public static function update_user_data( $data_key, $post_id, $value = '', $user_id = 0 ) {
if ( ! ( $user_id > 0 ) ) {
$user_id = get_current_user_id();
}
$supported_post_types = array( 'course', 'lesson' );
$post_type = get_post_type( $post_id );
if ( empty( $post_id ) || empty( $data_key )
|| ! is_int( $post_id ) || ! ( intval( $post_id ) > 0 ) || ! ( intval( $user_id ) > 0 )
|| ! get_userdata( $user_id )
|| ! in_array( $post_type, $supported_post_types, true ) ) {
return false;
}
// check if there and existing Sensei status on this post type if not create it
// and get the activity ID
$status_function = 'user_' . $post_type . '_status';
$sensei_user_status = self::$status_function( $post_id, $user_id );
if ( ! isset( $sensei_user_status->comment_ID ) ) {
$start_function = 'user_start_' . $post_type;
self::$start_function( $user_id, $post_id );
}
if ( 'course' === $post_type ) {
$course_progress_repository = Sensei()->course_progress_repository_factory->create_comments_based_repository();
$course_progress = $course_progress_repository->get( $post_id, $user_id );
$sensei_user_activity_id = $course_progress->get_id();
} else {
$lesson_progress_repository = Sensei()->lesson_progress_repository_factory->create_comments_based_repository();
$lesson_progress = $lesson_progress_repository->get( $post_id, $user_id );
$sensei_user_activity_id = $lesson_progress->get_id();
}
// store the data
$success = (bool) update_comment_meta( $sensei_user_activity_id, $data_key, $value );
return $success;
}
/**
* Get the user data stored on the passed in post type
*
* This function gets the comment meta on the lesson or course status
*
* @since 1.7.4
*
* @param $data_key
* @param $post_id
* @param int $user_id
*
* @return mixed $user_data_value
*/
public static function get_user_data( $data_key, $post_id, $user_id = 0 ) {
$user_data_value = true;
if ( ! ( $user_id > 0 ) ) {
$user_id = get_current_user_id();
}
$supported_post_types = array( 'course', 'lesson' );
$post_type = get_post_type( $post_id );
if ( empty( $post_id ) || empty( $data_key )
|| ! ( intval( $post_id ) > 0 ) || ! ( intval( $user_id ) > 0 )
|| ! get_userdata( $user_id )
|| ! in_array( $post_type, $supported_post_types, true ) ) {
return false;
}
// check if there and existing Sensei status on this post type if not create it
// and get the activity ID
$status_function = 'user_' . $post_type . '_status';
$sensei_user_status = self::$status_function( $post_id, $user_id );
if ( ! isset( $sensei_user_status->comment_ID ) ) {
return false;
}
$sensei_user_activity_id = $sensei_user_status->comment_ID;
$user_data_value = get_comment_meta( $sensei_user_activity_id, $data_key, true );
return $user_data_value;
}
/**
* Delete the Sensei user data for the given key, Sensei post type and user combination.
*
* @param int $data_key
* @param int $post_id
* @param int $user_id
*
* @return bool $deleted
*/
public static function delete_user_data( $data_key, $post_id, $user_id ) {
$deleted = true;
if ( ! ( $user_id > 0 ) ) {
$user_id = get_current_user_id();
}
$supported_post_types = array( 'course', 'lesson' );
$post_type = get_post_type( $post_id );
if ( empty( $post_id ) || empty( $data_key )
|| ! is_int( $post_id ) || ! ( intval( $post_id ) > 0 ) || ! ( intval( $user_id ) > 0 )
|| ! get_userdata( $user_id )
|| ! in_array( $post_type, $supported_post_types, true ) ) {
return false;
}
// check if there and existing Sensei status on this post type if not create it
// and get the activity ID
$status_function = 'user_' . $post_type . '_status';
$sensei_user_status = self::$status_function( $post_id, $user_id );
if ( ! isset( $sensei_user_status->comment_ID ) ) {
return false;
}
$sensei_user_activity_id = $sensei_user_status->comment_ID;
$deleted = delete_comment_meta( $sensei_user_activity_id, $data_key );
return $deleted;
}
/**
* The function creates a drop down. Never write up a Sensei select statement again.
*
* @since 1.8.0
*
* @param string $selected_value
* @param $options{
* @type string $value the value saved in the database
* @type string $option what the user will see in the list of items
* }
* @param array $attributes{
* @type string $attribute type such name or id etc.
* @type string $value
* }
* @param bool $enable_none_option
*
* @return string $drop_down_element
*/
public static function generate_drop_down( $selected_value, $options = array(), $attributes = array(), $enable_none_option = true ) {
$drop_down_element = '';
// setup the basic attributes
if ( ! isset( $attributes['name'] ) || empty( $attributes['name'] ) ) {
$attributes['name'] = 'sensei-options';
}
if ( ! isset( $attributes['id'] ) || empty( $attributes['id'] ) ) {
$attributes['id'] = 'sensei-options';
}
if ( ! isset( $attributes['class'] ) || empty( $attributes['class'] ) ) {
$attributes['class'] = 'chosen_select widefat';
}
// create element attributes
$combined_attributes = '';
foreach ( $attributes as $attribute => $value ) {
$combined_attributes .= $attribute . '="' . esc_attr( $value ) . '"' . ' ';
}
// create the select element
$drop_down_element .= '<select ' . $combined_attributes . ' >' . "\n";
// show the none option if the client requested
if ( $enable_none_option ) {
$drop_down_element .= '<option value="">' . esc_html__( 'None', 'sensei-lms' ) . '</option>';
}
if ( $options ) {
foreach ( $options as $value => $option ) {
$element = '';
$element .= '<option value="' . esc_attr( $value ) . '"';
$element .= selected( $value, $selected_value, false ) . '>';
$element .= esc_html( $option ) . '</option>' . "\n";
// add the element to the select html
$drop_down_element .= $element;
}
}
$drop_down_element .= '</select>' . "\n";
return wp_kses(
$drop_down_element,
array(
'option' => array(
'selected' => array(),
'value' => array(),
),
'select' => array(
'class' => array(),
'id' => array(),
'name' => array(),
'style' => array(),
),
)
);
}
/**
* Wrapper for the default php round() function.
* This allows us to give more control to a user on how they can round Sensei
* decimals passed through this function.
*
* @since 1.8.5
*
* @param float $val Value to round.
* @param int $precision Precision.
* @param int $mode Round mode.
* @param string $context Context.
*
* @return double $val
*/
public static function round( $val, $precision = 0, $mode = PHP_ROUND_HALF_UP, $context = '' ) {
/**
* Filter the round precision.
*
* Change the precision for the Sensei_Utils::round function.
* the precision given will be passed into the php round function
*
* @since 1.8.5
*
* @hook sensei_round_precision
*
* @param {int} $precision Precision.
* @param {float} $value Value to round.
* @param {string} $context Context.
* @param {int} $mode Round mode.
* @return {int} Filtered precision.
*/
$precision = apply_filters( 'sensei_round_precision', $precision, $val, $context, $mode );
/**
* Filter round mode.
*
* Change the mode for the Sensei_Utils::round function.
* the mode given will be passed into the php round function
*
* This applies only to PHP version 5.3.0 and greater
*
* @since 1.8.5
*
* @hook sensei_round_mode
*
* @param {int} $mode Round mode.
* @param {float} $value Value to round.
* @param {string} $context Context.
* @param {int} $precision Precision.
* @return {int} Filtered round mode.
*/
$mode = apply_filters( 'sensei_round_mode', $mode, $val, $context, $precision );
return round( $val, $precision, $mode );
}
/**
* Returns the current url with all the query vars.
*
* @since 1.9.0
* @deprecated 4.0.2
* @return string $url
*/
public static function get_current_url() {
_deprecated_function( __METHOD__, '4.0.2' );
global $wp;
$current_url = trailingslashit( home_url( $wp->request ) );
if ( isset( $_GET ) ) {
foreach ( $_GET as $param => $val ) {
$current_url = add_query_arg( $param, $val, $current_url );
}
}
return $current_url;
}
/**
* Get the course id of the current post.
*
* @return int|null The course id or null if it was not found.
*/
public static function get_current_course() {
global $post;
$post_type = get_post_type( $post );
$course_id = null;
switch ( $post_type ) {
case 'course':
$course_id = $post->ID;
break;
case 'lesson':
$course_id = Sensei()->lesson->get_course_id( $post->ID );
break;
case 'quiz':
$lesson_id = (int) get_post_meta( $post->ID, '_quiz_lesson', true );
$course_id = $lesson_id ? Sensei()->lesson->get_course_id( $lesson_id ) : null;
break;
}
return $course_id ? absint( $course_id ) : null;
}
/**
* Get the lesson id of the current post, if it's a lesson or quiz.
*
* @return int|null The lesson id or null if it was not found.
*/
public static function get_current_lesson() {
global $post;
if ( empty( $post ) ) {
return null;
}
switch ( get_post_type( $post ) ) {
case 'lesson':
return $post->ID;
case 'quiz':
return Sensei()->quiz->get_lesson_id( $post->ID );
}
return null;
}
/**
* Restore the global WP_Query
*
* @since 1.9.0
*/
public static function restore_wp_query() {
wp_reset_query();
}
/**
* Merge two arrays in a zip like fashion.
* If one array is longer than the other the elements will be apended
* to the end of the resulting array.
*
* @since 1.9.0
*
* @param array $array_a
* @param array $array_b
* @return array $merged_array
*/
public static function array_zip_merge( $array_a, $array_b ) {
if ( ! is_array( $array_a ) || ! is_array( $array_b ) ) {
trigger_error( 'array_zip_merge requires both arrays to be indexed arrays ' );
}
$merged_array = array();
$total_elements = count( $array_a ) + count( $array_b );
// Zip arrays
for ( $i = 0; $i < $total_elements; $i++ ) {
// if has an element at current index push a on top
if ( isset( $array_a[ $i ] ) ) {
$merged_array[] = $array_a[ $i ];
}
// next if $array_b has an element at current index push a on top of the element
// from a if there was one, if not the element before that.
if ( isset( $array_b[ $i ] ) ) {
$merged_array[] = $array_b[ $i ];
}
}
return $merged_array;
}
/**
* What type of request is this?
*
* @param string $type admin, ajax, cron or frontend.
* @return bool
*/
public static function is_request( $type ) {
switch ( $type ) {
case 'admin':
return is_admin();
case 'ajax':
return defined( 'DOING_AJAX' );
case 'cron':
return defined( 'DOING_CRON' );
case 'frontend':
return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' );
}
}
/**
* Check if this is a REST API request.
*
* @since 4.10.0
*
* @return bool
*/
public static function is_rest_request(): bool {
return defined( 'REST_REQUEST' ) && REST_REQUEST;
}
/**
* Check if this is a frontend request.
*
* @since 4.19.2
*
* @return bool
*/
public static function is_frontend_request(): bool {
return ! self::is_rest_request() && ! is_admin();
}
/**
* Add user to course.
*
* @param int $user_id The user ID.
* @param int $course_id The course ID.
* @return int Returns the ID of the user course progress or false on failure. The progress ID might have different meanings depending on the underlying implementation.
*/
public static function start_user_on_course( $user_id, $course_id ) {
$course_progress = Sensei()->course_progress_repository->create( $course_id, $user_id );
// Allow further actions.
$course_metadata = [
'percent' => 0,
'complete' => 0,
];
foreach ( $course_metadata as $key => $value ) {
update_comment_meta( $course_progress->get_id(), $key, $value );
}
/**
* Fires when a user starts a course.
*
* @hook sensei_user_course_start
*
* @param {int} $user_id The user ID.
* @param {int} $course_id The course ID.
*/
do_action( 'sensei_user_course_start', $user_id, $course_id );
return $course_progress->get_id();
}
public static function is_plugin_present_and_activated( $plugin_class_to_look_for, $plugin_registered_path ) {
$active_plugins = (array) get_option( 'active_plugins', array() );
if ( is_multisite() ) {
$active_sitewide_plugins = get_site_option( 'active_sitewide_plugins' );
if ( $active_sitewide_plugins ) {
$active_plugins = array_merge( $active_plugins, $active_sitewide_plugins );
}
}
$plugin_present_and_activated = in_array( $plugin_registered_path, $active_plugins ) || array_key_exists( $plugin_registered_path, $active_plugins );
return class_exists( $plugin_class_to_look_for ) || $plugin_present_and_activated;
}
/**
* Check if WooCommerce is installed.
*
* @since 3.11.0
*
* @return bool
*/
public static function is_woocommerce_installed() {
return file_exists( WP_PLUGIN_DIR . '/woocommerce/woocommerce.php' );
}
/**
* Checks if the given version pf WooCommerce plugin is installed and activated.
*
* @param string $minimum_version
*
* @return bool
* @since Sensei 3.2.0
*/
public static function is_woocommerce_active( $minimum_version = null ) {
$is_active = self::is_plugin_present_and_activated( 'Woocommerce', 'woocommerce/woocommerce.php' );
if ( ! $is_active ) {
return false;
}
if ( null !== $minimum_version ) {
return version_compare( WC()->version, $minimum_version, '>=' );
}
return true;
}
/**
* Get WooCommerce plugin information.
*
* @return array WooCommerce information.
*/
public static function get_woocommerce_plugin_information() {
$wc_information = get_transient( self::WC_INFORMATION_TRANSIENT );
if ( false === $wc_information ) {
if ( ! function_exists( 'plugins_api' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
}
$wc_slug = 'woocommerce';
$plugin_information = plugins_api(
'plugin_information',
[
'slug' => $wc_slug,
'fields' => [
'short_description' => true,
'description' => false,
'sections' => false,
'tested' => false,
'requires' => false,
'requires_php' => false,
'rating' => false,
'ratings' => false,
'downloaded' => false,
'downloadlink' => false,
'last_updated' => false,
'added' => false,
'tags' => false,
'compatibility' => false,
'homepage' => false,
'versions' => false,
'donate_link' => false,
'reviews' => false,
'banners' => false,
'icons' => false,
'active_installs' => false,
'group' => false,
'contributors' => false,
],
]
);
$wc_information = (object) [
'product_slug' => $wc_slug,
'title' => $plugin_information->name,
'excerpt' => $plugin_information->short_description,
'plugin_file' => 'woocommerce/woocommerce.php',
'link' => 'https://wordpress.org/plugins/' . $wc_slug,
'unselectable' => true,
'version' => $plugin_information->version,
];
set_transient( self::WC_INFORMATION_TRANSIENT, $wc_information, DAY_IN_SECONDS );
}
// Add installed properties to the object.
$wc_information = Sensei_Extensions::instance()->add_installed_extensions_properties( [ $wc_information ] );
$wc_information = $wc_information[0];
return $wc_information;
}
/**
* Get data used for WooCommerce.com purchase redirect.
*
* @deprecated 4.8.0
*
* @return array The data.
*/
public static function get_woocommerce_connect_data() {
_deprecated_function( __METHOD__, '4.8.0' );
$wc_params = [];
$is_woocommerce_installed = self::is_woocommerce_active( '3.7.0' ) && class_exists( 'WC_Admin_Addons' );
if ( $is_woocommerce_installed ) {
$wc_params = WC_Admin_Addons::get_in_app_purchase_url_params();
} else {
$wc_info = self::get_woocommerce_plugin_information();
$wc_params = [
'wccom-site' => site_url(),
'wccom-woo-version' => $wc_info->version,
'wccom-connect-nonce' => wp_create_nonce( 'connect' ),
];
}
$wc_params['wccom-back'] = rawurlencode( 'admin.php' );
return $wc_params;
}
/**
* Hard - Resets a Learner's Course Progress
*
* @param $course_id int
* @param $user_id int
* @return bool
*/
public static function reset_course_for_user( $course_id, $user_id ) {
self::sensei_remove_user_from_course( $course_id, $user_id );
if ( ! Sensei_Course::is_user_enrolled( $course_id, $user_id ) ) {
return true;
}
return false !== self::user_start_course( $user_id, $course_id );
}
/**
* @param $setting_name string
* @param null|string $filter_to_apply
* @return bool
*/
public static function get_setting_as_flag( $setting_name, $filter_to_apply = null ) {
$setting_on = false;
if ( isset( Sensei()->settings->settings[ $setting_name ] ) ) {
$setting_on = (bool) Sensei()->settings->settings[ $setting_name ];
}
return ( null !== $filter_to_apply ) ? (bool) apply_filters( $filter_to_apply, $setting_on ) : $setting_on;
}
/**
* Determine whether to show the lessons on the single course page.
*
* @since 2.2.0
*
* @param int|false $course_id Course ID.
* @return bool Whether to show the lessons. Default true.
*/
public static function show_course_lessons( $course_id ) {
/**
* Set the visibility of lessons on the single course page.
*
* @since 2.2.0
*
* @hook sensei_course_show_lessons
*
* @param {bool} $show_lessons Whether the lessons should be shown. Default true.
* @param {int|false} $course_id Course ID.
* @return {bool} Filtered visibility of lessons.
*/
return apply_filters( 'sensei_course_show_lessons', true, $course_id );
}
/**
* Determine if the current page is a Sensei learner profile page.
*
* @since 3.2.0
*
* @return bool True if the current page is a Sensei learner profile page.
*/
public static function is_learner_profile_page() {
global $wp_query;
return isset( $wp_query->query_vars['learner_profile'] );
}
/**
* Determine if the current page is a Sensei course results page.
*
* @since 3.2.0
*
* @return bool True if the current page is a Sensei course results page.
*/
public static function is_course_results_page() {
global $wp_query;
return isset( $wp_query->query_vars['course_results'] );
}
/**
* Determine if the current page is a Sensei teacher archive page.
*
* @access public
* @since 3.2.0
* @return bool True if the current page is a Sensei teacher archive page.
*/
public static function is_teacher_archive_page() {
if ( is_author()
&& Sensei()->teacher->is_a_teacher( get_query_var( 'author' ) )
&& ! user_can( get_query_var( 'author' ), 'manage_options' ) ) {
return true;
}
return false;
}
/**
* Output the current query params as hidden inputs.
*
* @since 4.2.0
*
* @param array $excluded The query params that should be excluded.
* @param string $url The URL to parse the query params from. If empty, the current URL will be used.
* @param bool $echo Whether to echo the output or return it.
*
* @return string|void The HTML output if `$echo` is false, null otherwise.
*/
public static function output_query_params_as_inputs( array $excluded = [], string $url = '', bool $echo = true ) {
// phpcs:ignore WordPress.Security.NonceVerification -- The nonce should be checked before calling this method.
$query_params = $_GET;
if ( $url ) {
parse_str( (string) wp_parse_url( $url, PHP_URL_QUERY ), $query_params );
}
$output = '';
foreach ( $query_params as $name => $value ) {
if ( in_array( $name, $excluded, true ) ) {
continue;
}
$output .= '<input type="hidden" name="' . esc_attr( $name ) . '" value="' . esc_attr( wp_unslash( $value ) ) . '">';
}
if ( ! $echo ) {
return $output;
}
echo wp_kses(
$output,
[
'input' => [
'type' => [],
'name' => [],
'value' => [],
],
]
);
}
/**
* Format the last activity date to a more readable form.
*
* @since 4.4.0
*
* @param string $date The last activity date.
*
* @return string The formatted last activity date.
*/
public static function format_last_activity_date( string $date ) {
$timezone = new DateTimeZone( 'GMT' );
$now = Sensei()->clock->now( $timezone );
$date = new DateTime( $date, $timezone );
$diff_in_days = $now->diff( $date )->days;
// Show a human readable date if activity is within 6 days.
if ( $diff_in_days < 7 ) {
return sprintf(
/* translators: Time difference between two dates. %s: Number of seconds/minutes/etc. */
__( '%s ago', 'sensei-lms' ),
human_time_diff( $date->getTimestamp() )
);
}
return wp_date( get_option( 'date_format' ), $date->getTimestamp(), $timezone );
}
/**
* Render a video embed.
*
* @param string $url The URL for the video embed.
*
* @return string an embeddable HTML string.
*/
public static function render_video_embed( $url ) {
$allowed_html = array(
'embed' => array(),
'iframe' => array(
'title' => array(),
'width' => array(),
'height' => array(),
'src' => array(),
'frameborder' => array(),
'allowfullscreen' => array(),
),
'video' => Sensei_Wp_Kses::get_video_html_tag_allowed_attributes(),
);
if ( 'http' === substr( $url, 0, 4 ) ) {
// V2 - make width and height a setting for video embed.
$url = wp_oembed_get( esc_url( $url ) );
$url = do_shortcode( html_entity_decode( $url ) );
}
return Sensei_Wp_Kses::maybe_sanitize( $url, $allowed_html );
}
/**
* Gets the HTML content from the Featured Video for a lesson.
*
* @since 4.7.0
*
* @param string $post_id the post ID.
*
* @return string|null The featured video HTML output if it exists.
*/
public static function get_featured_video_html( $post_id = null ) {
$post = get_post( $post_id );
if ( empty( $post ) ) {
return null;
}
if ( has_block( 'sensei-lms/featured-video', $post ) ) {
$blocks = parse_blocks( $post->post_content );
foreach ( $blocks as $block ) {
if ( 'sensei-lms/featured-video' === $block['blockName'] ) {
return render_block( $block );
}
}
}
$video_embed = get_post_meta( $post->ID, '_lesson_video_embed', true );
return $video_embed ? self::render_video_embed( $video_embed ) : null;
}
/**
* Get the featured video thumbnail URL from a Post's metadata.
*
* @param int $post_id The Post ID.
* @return string The video thumbnail URL.
*/
public static function get_featured_video_thumbnail_url( $post_id ) {
return get_post_meta( $post_id, '_featured_video_thumbnail', true );
}
/**
* Tells if the website is hosted on the wp.com atomic site.
*/
public static function is_atomic_platform(): bool {
return defined( 'ATOMIC_SITE_ID' ) && ATOMIC_SITE_ID && defined( 'ATOMIC_CLIENT_ID' ) && ATOMIC_CLIENT_ID;
}
/**
* Get count of users for a provided role.
*
* @param string $role Slug of the Role.
* @return int Count of users having the provided role.
*/
public static function get_user_count_for_role( $role ) {
return count(
Sensei_Temporary_User::get_all_users(
[
'fields' => 'ID',
'role' => $role,
]
)
);
}
/**
* Tells if the current site is hosted in wordpress.com and the
* plan includes an active subscription for a paid Sensei product.
*
* @return bool {bool} If there is an active WPCOM subscription or not.
* @since 4.11.0
*/
public static function has_wpcom_subscription(): bool {
$subscriptions = get_option( 'wpcom_active_subscriptions', [] );
/**
* Filter to allow adding products slugs to check if it has an active WPCOM subscription.
*
* @since 4.11.0
*
* @hook sensei_wpcom_product_slugs
*
* @param {array} $products Array of products slugs to check if it has an active WPCOM subscription.
* @return {array} Filtered array of products slugs.
*/
$product_slugs = apply_filters( 'sensei_wpcom_product_slugs', [] );
foreach ( $product_slugs as $product_slug ) {
if ( array_key_exists( $product_slug, $subscriptions ) ) {
return true;
}
}
return false;
}
/**
* Gets the id for the last lesson the user was working on, or the next lesson, or
* the course id as fallback for fresh users or courses with no lessons.
*
* @param int $course_id Id of the course.
* @param int $user_id Id of the user.
*
* @since 4.12.0
*
* @return int
*/
public static function get_target_page_post_id_for_continue_url( $course_id, $user_id ) {
$course_lessons = Sensei()->course->course_lessons( $course_id, 'publish', 'ids' );
if ( empty( $course_lessons ) ) {
return $course_id;
}
// First try to get the lesson the user started or updated last.
$progress_args = array(
'lesson_id' => $course_lessons,
'user_id' => $user_id,
'status' => array( 'in-progress' ),
'orderby' => 'updated_at',
'order' => 'DESC',
'number' => 1,
);
$last_progress = Sensei()->lesson_progress_repository->find( $progress_args );
if ( count( $last_progress ) > 0 ) {
return $last_progress[0]->get_lesson_id();
}
// If there is no such lesson, get the first lesson that the user has not yet started.
$completed_lessons = Sensei()->course->get_completed_lesson_ids( $course_id, $user_id );
$not_completed_lessons = array_diff( $course_lessons, $completed_lessons );
if ( $not_completed_lessons ) {
return current( $not_completed_lessons );
}
return $course_id;
}
/**
* Check if the current theme supports Full Site Editing (FSE).
*
* @since 4.16.1
*
* @return bool True if FSE is supported, false otherwise.
*/
public static function is_fse_theme() {
return function_exists( 'wp_is_block_theme' ) && wp_is_block_theme();
}
/**
* Check if the current screen is a site editor page.
*
* @return bool
*/
public static function is_site_editor() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
return ! empty( $screen ) && in_array( $screen->id, [ 'widgets', 'site-editor', 'customize', 'appearance_page_gutenberg-edit-site' ], true );
}
}
/**
* Class WooThemes_Sensei_Utils
*
* @ignore only for backward compatibility
* @since 1.9.0
*/
class WooThemes_Sensei_Utils extends Sensei_Utils{}