1326 lines
37 KiB
PHP
1326 lines
37 KiB
PHP
<?php
|
|
/**
|
|
* LearnDash Sensor helper.
|
|
*
|
|
* @package wsal
|
|
* @subpackage sensors
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace WSAL\WP_Sensors\Helpers;
|
|
|
|
use WSAL\Helpers\WP_Helper;
|
|
|
|
// Exit if accessed directly.
|
|
if ( ! \defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
if ( ! \class_exists( '\WSAL\WP_Sensors\Helpers\LearnDash_Helper' ) ) {
|
|
/**
|
|
* Helper Sensor class for LearnDash.
|
|
*
|
|
* @package wsal
|
|
* @subpackage sensors-helpers
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
class LearnDash_Helper {
|
|
|
|
/**
|
|
* Stores if the plugin is active.
|
|
*
|
|
* @var bool
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
private static $plugin_active = null;
|
|
|
|
/**
|
|
* Stores if the plugin is active for sensors.
|
|
*
|
|
* @var bool
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
private static $plugin_active_for_sensors = null;
|
|
|
|
/**
|
|
* Check if LearnDash is active.
|
|
*
|
|
* @return bool
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function is_learndash_active() {
|
|
if ( null === self::$plugin_active ) {
|
|
if ( \function_exists( 'learndash_get_post_type_slug' ) ) {
|
|
self::$plugin_active = true;
|
|
} else {
|
|
self::$plugin_active = false;
|
|
}
|
|
}
|
|
|
|
return self::$plugin_active;
|
|
}
|
|
|
|
/**
|
|
* Check if plugin alerts should be loaded.
|
|
*
|
|
* @return boolean
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function load_alerts_for_sensor(): bool {
|
|
if ( null === self::$plugin_active_for_sensors ) {
|
|
self::$plugin_active_for_sensors = ( WP_Helper::is_plugin_active( 'sfwd-lms/sfwd_lms.php' ) );
|
|
}
|
|
|
|
return self::$plugin_active_for_sensors;
|
|
}
|
|
|
|
/**
|
|
* Register custom event objects for WSAL.
|
|
*
|
|
* @param array $objects - Array of objects current registered within WSAL.
|
|
*
|
|
* @return array
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function wsal_learndash_add_custom_event_objects( $objects ) {
|
|
$new_objects = array(
|
|
'learndash_courses' => \esc_html__( 'LearnDash Courses', 'wp-security-audit-log' ),
|
|
'learndash_lessons' => \esc_html__( 'LearnDash Lessons', 'wp-security-audit-log' ),
|
|
'learndash_topics' => \esc_html__( 'LearnDash Topics', 'wp-security-audit-log' ),
|
|
'learndash_quizzes' => \esc_html__( 'LearnDash Quizzes', 'wp-security-audit-log' ),
|
|
'learndash_groups' => \esc_html__( 'LearnDash Groups', 'wp-security-audit-log' ),
|
|
'learndash_settings' => \esc_html__( 'LearnDash Settings', 'wp-security-audit-log' ),
|
|
'learndash_certificates' => \esc_html__( 'LearnDash Certificates', 'wp-security-audit-log' ),
|
|
'learndash_students' => \esc_html__( 'LearnDash Students', 'wp-security-audit-log' ),
|
|
);
|
|
|
|
// combine the two arrays.
|
|
$objects = \array_merge( $objects, $new_objects );
|
|
|
|
return $objects;
|
|
}
|
|
|
|
/**
|
|
* Returns a list of custom post types associated with LearnDash.
|
|
*
|
|
* @return array - List of custom post types.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_custom_post_types(): array {
|
|
return array( 'sfwd-courses', 'sfwd-lessons', 'sfwd-topic', 'sfwd-quiz', 'groups', 'sfwd-question', 'sfwd-certificates' );
|
|
}
|
|
|
|
/**
|
|
* Returns a list of custom taxonomies associated with LearnDash.
|
|
*
|
|
* @return array - List of custom taxonomies.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_custom_taxonomies(): array {
|
|
return array( 'ld_course_category', 'ld_course_tag', 'ld_lesson_category', 'ld_lesson_tag', 'ld_topic_category', 'ld_topic_tag', 'ld_quiz_category', 'ld_group_category', 'ld_group_tag' );
|
|
}
|
|
|
|
/**
|
|
* Ignore Learndash CPTs for the WP Content sensors.
|
|
*
|
|
* @param array $post_types - An array of default post_types.
|
|
*
|
|
* @return array
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function wsal_learndash_add_custom_ignored_cpt( $post_types ) {
|
|
$new_post_types = self::get_custom_post_types();
|
|
|
|
// combine the two arrays.
|
|
$post_types = \array_merge( $post_types, $new_post_types );
|
|
|
|
return $post_types;
|
|
}
|
|
|
|
/**
|
|
* Gets the filename of the plugin this extension is targeting.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_plugin_filename(): string {
|
|
return 'sfwd-lms/sfwd_lms.php';
|
|
}
|
|
|
|
/**
|
|
* Get the author name from a post.
|
|
*
|
|
* @param \WP_Post $post - Post object.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_author_name( $post ): string {
|
|
$post_author = \get_userdata( (int) $post->post_author );
|
|
return $post_author ? $post_author->user_login : self::get_ld_unknown_string();
|
|
}
|
|
|
|
/**
|
|
* Get the author display name from a post.
|
|
*
|
|
* @param \WP_Post $post - Post object.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_author_display_name( $post ): string {
|
|
$author_id = (int) $post->post_author;
|
|
$author_data = \get_userdata( $author_id );
|
|
$author_display_name = $author_data ? $author_data->display_name : self::get_ld_unknown_string();
|
|
|
|
return $author_display_name;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get WordPress categories as comma-separated string.
|
|
*
|
|
* @param int $post_id - Post ID.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_post_categories( $post_id ): string {
|
|
$categories = \wp_get_post_terms( $post_id, 'category', array( 'fields' => 'names' ) );
|
|
return ! empty( $categories ) && ! \is_wp_error( $categories ) ? \implode( ', ', $categories ) : self::get_ld_none_string();
|
|
}
|
|
|
|
/**
|
|
* Get the price type from a Learndash post.
|
|
*
|
|
* Retrieves the LearnDash price type meta value (e.g., 'open', 'free', 'paynow', 'subscribe', 'closed').
|
|
*
|
|
* @param int $post_id - Post ID.
|
|
*
|
|
* @return string - The price type as string, e.g. 'free' or 'Not set' if not available.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_post_price_type( $post_id ) {
|
|
return \get_post_meta( $post_id, '_ld_price_type', true ) ?? \esc_html__( 'Not set', 'wp-security-audit-log' );
|
|
}
|
|
|
|
/**
|
|
* Get LearnDash post categories as comma-separated string.
|
|
*
|
|
* @param int $post_id - Post ID.
|
|
* @param string $post_type - Post type.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_post_categories( $post_id, $post_type ): string {
|
|
|
|
$taxonomy_key = 'ld_course_category';
|
|
|
|
if ( 'sfwd-lessons' === $post_type ) {
|
|
$taxonomy_key = 'ld_lesson_category';
|
|
} elseif ( 'sfwd-topic' === $post_type ) {
|
|
$taxonomy_key = 'ld_topic_category';
|
|
} elseif ( 'sfwd-quiz' === $post_type ) {
|
|
$taxonomy_key = 'ld_quiz_category';
|
|
} elseif ( 'groups' === $post_type ) {
|
|
$taxonomy_key = 'ld_group_category';
|
|
}
|
|
|
|
$categories = \wp_get_post_terms( $post_id, $taxonomy_key, array( 'fields' => 'names' ) );
|
|
|
|
if ( ! empty( $categories ) && ! \is_wp_error( $categories ) ) {
|
|
$categories = \implode( ', ', $categories );
|
|
} else {
|
|
$categories = self::get_ld_none_string();
|
|
}
|
|
|
|
return $categories;
|
|
}
|
|
|
|
/**
|
|
* Build post event variables.
|
|
*
|
|
* @param \WP_Post $post - Post object.
|
|
*
|
|
* @return array
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function build_ld_post_event_variables( \WP_Post $post ): array {
|
|
$variables = array(
|
|
'PostTitle' => \esc_html( $post->post_title ),
|
|
'PostID' => $post->ID,
|
|
'PostAuthor' => self::get_author_name( $post ),
|
|
'Categories' => self::get_post_categories( $post->ID ),
|
|
'LdPostCategory' => self::get_ld_post_categories( $post->ID, $post->post_type ),
|
|
);
|
|
|
|
if ( in_array( $post->post_type, array( 'sfwd-courses', 'groups' ), true ) ) {
|
|
$variables['PriceType'] = self::get_ld_post_price_type( $post->ID );
|
|
}
|
|
|
|
return $variables;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Get term names from term IDs.
|
|
*
|
|
* @param array $term_ids - Array of term IDs.
|
|
* @param string $taxonomy - Taxonomy slug.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_term_names_from_ids( $term_ids, $taxonomy ): string {
|
|
$term_names = array();
|
|
|
|
foreach ( $term_ids as $term_id ) {
|
|
$term = \get_term_by( 'term_taxonomy_id', $term_id, $taxonomy );
|
|
if ( $term && ! \is_wp_error( $term ) ) {
|
|
$term_names[] = $term->name;
|
|
}
|
|
}
|
|
|
|
return ! empty( $term_names ) ? \implode( ', ', $term_names ) : '';
|
|
}
|
|
|
|
/**
|
|
* Format lessons, topics, and quizzes under ld_course_steps metadata for later comparison.
|
|
*
|
|
* @param int $post_id - Post ID.
|
|
* @param string $meta_key - Meta key being updated.
|
|
*
|
|
* @return array The course steps metadata.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function format_course_steps_metadata( $post_id, $meta_key ): array {
|
|
$old_meta_record = \get_post_meta( $post_id, $meta_key, true );
|
|
$unserialized_meta = \maybe_unserialize( $old_meta_record );
|
|
|
|
$lessons = array();
|
|
$topics = array();
|
|
$quizzes = array();
|
|
|
|
if ( \is_array( $unserialized_meta ) || \is_object( $unserialized_meta ) ) {
|
|
$lessons = self::extract_learndash_items( $unserialized_meta, 'sfwd-lessons' );
|
|
$topics = self::extract_learndash_items( $unserialized_meta, 'sfwd-topic' );
|
|
$quizzes = self::extract_learndash_items( $unserialized_meta, 'sfwd-quiz' );
|
|
}
|
|
|
|
$course_steps_meta = array(
|
|
'lessons' => array_keys( $lessons ),
|
|
'topics' => $topics,
|
|
'quizzes' => array_keys( $quizzes ),
|
|
'raw_metadata' => $unserialized_meta,
|
|
);
|
|
|
|
return $course_steps_meta;
|
|
}
|
|
|
|
/**
|
|
* Extracts lesson or topic IDs from LearnDash course steps metadata.
|
|
*
|
|
* @param array $data - Unserialized course steps metadata (array/object).
|
|
* @param string $post_type - The post type to extract ('sfwd-lessons' or 'sfwd-topic').
|
|
*
|
|
* @return array - Array of IDs. For lessons: associative array (id => data), for topics: numeric array of IDs.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function extract_learndash_items( $data, $post_type ): array {
|
|
return self::search_nested_ld_structure( $data, $post_type, array() );
|
|
}
|
|
|
|
/**
|
|
* Recursively searches nested structure for LearnDash items.
|
|
*
|
|
* @param mixed $node - Current node being examined.
|
|
* @param string $post_type - The post type to extract.
|
|
* @param array $items - Accumulated items found so far.
|
|
*
|
|
* @return array - Updated items array.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function search_nested_ld_structure( $node, $post_type, $items = array() ): array {
|
|
|
|
// Return early if this isn't an array.
|
|
if ( ! \is_array( $node ) ) {
|
|
return $items;
|
|
}
|
|
|
|
foreach ( $node as $key => $value ) {
|
|
if ( $post_type === $key && ( \is_array( $value ) || \is_object( $value ) ) ) {
|
|
foreach ( (array) $value as $item_id => $item_data ) {
|
|
if ( 'sfwd-lessons' === $post_type || 'sfwd-quiz' === $post_type ) {
|
|
$items[ (string) $item_id ] = $item_data;
|
|
} elseif ( 'sfwd-topic' === $post_type ) {
|
|
$items[] = (int) $item_id;
|
|
}
|
|
}
|
|
|
|
$items = self::search_nested_ld_structure( $value, $post_type, $items );
|
|
continue;
|
|
}
|
|
|
|
if ( \is_array( $value ) || \is_object( $value ) ) {
|
|
$items = self::search_nested_ld_structure( $value, $post_type, $items );
|
|
}
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
/**
|
|
* Get enrollment status label from course prerequisites or points settings.
|
|
*
|
|
* @param array $meta - Course meta data.
|
|
*
|
|
* @return string - Formatted enrollment requirement label.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_enrollment_status_label( $meta ): string {
|
|
|
|
$course_req = $meta['sfwd-courses_requirements_for_enrollment'] ?? self::get_ld_none_string();
|
|
$prereq_value = $meta['sfwd-courses_course_prerequisite'] ?? array();
|
|
|
|
// If requirements are based on points.
|
|
if ( 'course_points_enabled' === $course_req && isset( $meta['sfwd-courses_course_points_access'] ) ) {
|
|
return \esc_html__( 'Points: ', 'wp-security-audit-log' ) . (string) $meta['sfwd-courses_course_points_access'];
|
|
}
|
|
|
|
// If requirements are based on prerequisite courses.
|
|
if ( 'course_prerequisite_enabled' === $course_req ) {
|
|
$course_titles = array();
|
|
|
|
if ( is_array( $prereq_value ) ) {
|
|
// Extract course titles from course post IDs.
|
|
foreach ( $prereq_value as $prereq_id ) {
|
|
|
|
$title = \get_the_title( (int) $prereq_id );
|
|
|
|
if ( ! empty( $title ) ) {
|
|
$course_titles[] = $title;
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $course_titles ) ) {
|
|
return implode( ', ', $course_titles );
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fallback to None if we did not find any requirements.
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
/**
|
|
* Formats duration in seconds to HH MM format.
|
|
*
|
|
* @param mixed $seconds - Duration in seconds.
|
|
*
|
|
* @return string - Formatted duration string.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function format_duration_for_display( $seconds ): string {
|
|
$seconds_string = (string) $seconds;
|
|
|
|
if ( empty( $seconds_string ) ) {
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
$seconds_int = (int) $seconds;
|
|
$hours = (int) \floor( $seconds_int / 3600 );
|
|
$minutes = (int) \floor( ( $seconds_int % 3600 ) / 60 );
|
|
|
|
$parts = array();
|
|
|
|
if ( $hours > 0 ) {
|
|
$parts[] = \sprintf(
|
|
/* translators: %s: number of hours */
|
|
\_n( '%s hour', '%s hours', $hours, 'wp-security-audit-log' ),
|
|
\number_format_i18n( $hours )
|
|
);
|
|
}
|
|
|
|
if ( $minutes > 0 ) {
|
|
$parts[] = \sprintf(
|
|
/* translators: %d: number of minutes */
|
|
\_n( '%d minute', '%d minutes', $minutes, 'wp-security-audit-log' ),
|
|
\number_format_i18n( $minutes )
|
|
);
|
|
}
|
|
|
|
return implode( ', ', $parts );
|
|
}
|
|
|
|
/**
|
|
* Get taxonomy edit link.
|
|
*
|
|
* @param int $term_id - Term ID.
|
|
* @param string $taxonomy - Taxonomy slug.
|
|
* @param string $post_type - Post type (optional). If provided, uses term.php format with post_type.
|
|
*
|
|
* @return string|null
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_taxonomy_edit_link( $term_id, $taxonomy = 'post_tag', $post_type = '' ) {
|
|
if ( empty( $term_id ) ) {
|
|
return null;
|
|
}
|
|
|
|
if ( ! empty( $post_type ) ) {
|
|
$term_args = array(
|
|
'taxonomy' => $taxonomy,
|
|
'tag_ID' => $term_id,
|
|
'post_type' => $post_type,
|
|
);
|
|
|
|
return \add_query_arg( $term_args, \network_admin_url( 'term.php' ) );
|
|
}
|
|
|
|
// Use term.php format, standard WordPress format for taxonomy term list as fall back if we don't have a post type.
|
|
$term_args = array(
|
|
'taxonomy' => $taxonomy,
|
|
'tag_ID' => $term_id,
|
|
);
|
|
|
|
return \add_query_arg( $term_args, \network_admin_url( 'term.php' ) );
|
|
}
|
|
|
|
/**
|
|
* Build a map of quiz locations with their parent IDs across course/lesson/topic hierarchy.
|
|
*
|
|
* @param array $steps_data - The steps['h'] data structure as provided by Learndash 'ld_course_steps' metadata.
|
|
* @param int $post_id - Course ID.
|
|
*
|
|
* @return array - Array with 'course_level', 'lesson_level', 'topic_level' keys, each containing arrays of quiz_id/parent_id pairs.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function map_quizzes_by_level( $steps_data, $post_id ) {
|
|
$quiz_map = array(
|
|
'course_level' => array(),
|
|
'lesson_level' => array(),
|
|
'topic_level' => array(),
|
|
);
|
|
|
|
// Course-level quizzes.
|
|
if ( isset( $steps_data['sfwd-quiz'] ) && ! empty( $steps_data['sfwd-quiz'] ) ) {
|
|
foreach ( array_keys( $steps_data['sfwd-quiz'] ) as $quiz_id ) {
|
|
$quiz_map['course_level'][] = array(
|
|
'quiz_id' => $quiz_id,
|
|
'parent_id' => $post_id,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Lesson-level and topic-level quizzes.
|
|
if ( isset( $steps_data['sfwd-lessons'] ) && ! empty( $steps_data['sfwd-lessons'] ) ) {
|
|
foreach ( $steps_data['sfwd-lessons'] as $lesson_id => $lesson_data ) {
|
|
// Lesson-level quizzes.
|
|
if ( isset( $lesson_data['sfwd-quiz'] ) && ! empty( $lesson_data['sfwd-quiz'] ) ) {
|
|
foreach ( array_keys( $lesson_data['sfwd-quiz'] ) as $quiz_id ) {
|
|
$quiz_map['lesson_level'][] = array(
|
|
'quiz_id' => $quiz_id,
|
|
'parent_id' => $lesson_id,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Topic-level quizzes.
|
|
if ( isset( $lesson_data['sfwd-topic'] ) && ! empty( $lesson_data['sfwd-topic'] ) ) {
|
|
foreach ( $lesson_data['sfwd-topic'] as $topic_id => $topic_data ) {
|
|
if ( isset( $topic_data['sfwd-quiz'] ) && ! empty( $topic_data['sfwd-quiz'] ) ) {
|
|
foreach ( array_keys( $topic_data['sfwd-quiz'] ) as $quiz_id ) {
|
|
$quiz_map['topic_level'][] = array(
|
|
'quiz_id' => $quiz_id,
|
|
'parent_id' => $topic_id,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $quiz_map;
|
|
}
|
|
|
|
/**
|
|
* Get post titles from an array of post IDs.
|
|
*
|
|
* @param array $post_ids - Array of post IDs.
|
|
*
|
|
* @return array - Array of post titles.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_post_titles_array( array $post_ids ): array {
|
|
$titles = array();
|
|
|
|
$post_ids = \array_values( \array_filter( $post_ids ) );
|
|
|
|
foreach ( $post_ids as $post_id ) {
|
|
$titles[] = \get_the_title( $post_id );
|
|
}
|
|
|
|
return $titles;
|
|
}
|
|
|
|
/**
|
|
* Get all enrolled course IDs for a user from LearnDash enrollment meta.
|
|
*
|
|
* @param int $user_id - User ID.
|
|
*
|
|
* @return array - Array of course IDs.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_user_enrolled_courses( $user_id ) {
|
|
$all_meta = \get_user_meta( $user_id );
|
|
$course_ids = array();
|
|
|
|
foreach ( $all_meta as $meta_key => $meta_value ) {
|
|
if ( \preg_match( '/^course_(\d+)_access_from$/', $meta_key, $matches ) ) {
|
|
$course_ids[] = (int) $matches[1];
|
|
}
|
|
}
|
|
|
|
return $course_ids;
|
|
}
|
|
|
|
/**
|
|
* Get all enrolled group IDs for a user.
|
|
*
|
|
* @param int $user_id - User ID.
|
|
*
|
|
* @return array - Array of group IDs.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_user_enrolled_groups( $user_id ) {
|
|
if ( \function_exists( 'learndash_get_users_group_ids' ) ) {
|
|
return \learndash_get_users_group_ids( $user_id );
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
/**
|
|
* Checks if a new post is a duplicate of a source post.
|
|
*
|
|
* @param \WP_Post $new_post The new post object.
|
|
* @param int $source_post_id The ID of the source post.
|
|
*
|
|
* @return bool True if it is a duplicate, false otherwise.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function is_post_duplicate( $new_post, $source_post_id ) {
|
|
$source_post = \get_post( $source_post_id );
|
|
|
|
if ( ! $source_post || $source_post->post_type !== $new_post->post_type ) {
|
|
return false;
|
|
}
|
|
|
|
// Compare content.
|
|
if ( $new_post->post_content !== $source_post->post_content ) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Check if a post was just created (within the last few seconds).
|
|
*
|
|
* @param \WP_Post $post - Post object.
|
|
*
|
|
* @return bool - True if post was just created, false otherwise.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function is_post_just_created( $post ) {
|
|
if ( ! $post ) {
|
|
return false;
|
|
}
|
|
|
|
// Use get_post_datetime to handle timezones and GMT/local fallback.
|
|
$post_datetime = \get_post_datetime( $post, 'date', 'gmt' );
|
|
|
|
if ( ! $post_datetime ) {
|
|
return false;
|
|
}
|
|
|
|
$created_timestamp = $post_datetime->getTimestamp();
|
|
$current_timestamp = \time();
|
|
|
|
// For scheduled posts, use modified date as they are "created" in the future.
|
|
if ( 'future' === $post->post_status ) {
|
|
$mod_datetime = \get_post_datetime( $post, 'modified', 'gmt' );
|
|
if ( $mod_datetime ) {
|
|
$created_timestamp = $mod_datetime->getTimestamp();
|
|
}
|
|
}
|
|
|
|
$age = $current_timestamp - $created_timestamp;
|
|
|
|
// Check if created within last 30 seconds, let's also set -5 to allow for minor time differences.
|
|
return $age >= -5 && $age < 30;
|
|
}
|
|
|
|
/**
|
|
* Format lesson order for display: titles if 10 or fewer lessons, IDs if more than 10.
|
|
*
|
|
* @param array $lesson_ids - Ordered array of lesson IDs.
|
|
*
|
|
* @return string - Formatted comma-separated string of lesson titles or IDs.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function format_lesson_order_for_display( $lesson_ids ) {
|
|
if ( empty( $lesson_ids ) ) {
|
|
return '';
|
|
}
|
|
|
|
// If 10 or fewer lessons, show titles.
|
|
if ( \count( $lesson_ids ) <= 10 ) {
|
|
|
|
$titles = array();
|
|
|
|
foreach ( $lesson_ids as $lesson_id ) {
|
|
$titles[] = \get_the_title( $lesson_id );
|
|
}
|
|
return \implode( ', ', $titles );
|
|
}
|
|
|
|
// If more than 10 lessons, show IDs.
|
|
return \implode( ', ', $lesson_ids );
|
|
}
|
|
|
|
/**
|
|
* Get translated 'None' string for LearnDash fields.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_none_string() {
|
|
return \esc_html__( 'None', 'wp-security-audit-log' );
|
|
}
|
|
|
|
/**
|
|
* Get translated 'Unknown' string for LearnDash fields.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_unknown_string() {
|
|
return \esc_html__( 'Unknown', 'wp-security-audit-log' );
|
|
}
|
|
|
|
/**
|
|
* Get the student count label.
|
|
*
|
|
* @param int $count - Student count.
|
|
*
|
|
* @return string
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_student_count_label( $count ) {
|
|
/* translators: %d: Number of students */
|
|
return $count > 0 ? sprintf( \_n( '%d student', '%d students', $count, 'wp-security-audit-log' ), $count ) : self::get_ld_none_string();
|
|
}
|
|
|
|
/**
|
|
* Format date to a valid float value.
|
|
*
|
|
* @param mixed $date_value - Date value.
|
|
*
|
|
* @return float
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function cast_date_to_float( $date_value ): float {
|
|
if ( empty( $date_value ) || '0' === (string) $date_value ) {
|
|
return 0;
|
|
}
|
|
|
|
return (float) $date_value;
|
|
}
|
|
|
|
/**
|
|
* Determine if a LearnDash post was created via the LearnDash Course Builder.
|
|
*
|
|
* LearnDash saves posts created via the course builder in 2 steps:
|
|
* 1. First, an empty post with a default name and slug (e.g., "lesson", "topic", "quiz").
|
|
* 2. Second, it updates the post with the name and slug specified by the user.
|
|
*
|
|
* This method detects the second step by checking if:
|
|
* - The post slug changed from the default builder slug to a user-specified slug.
|
|
* - The post author remained the same (it's impossible to change author from the course builder).
|
|
*
|
|
* @param \WP_Post $post_before - Post object before the update.
|
|
* @param \WP_Post $post_after - Post object after the update.
|
|
*
|
|
* @return bool - True if post was created via the Course Builder, false otherwise.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function ld_post_created_via_ld_course_builder( $post_before, $post_after ) {
|
|
// List post types that can be created via the Learndash course builder.
|
|
$ld_builder_post_types = array( 'sfwd-lessons', 'sfwd-topic', 'sfwd-quiz' );
|
|
|
|
$default_post_title_from_builder = array(
|
|
'sfwd-lessons' => 'Lesson',
|
|
'sfwd-topic' => 'Topic',
|
|
'sfwd-quiz' => 'Quiz',
|
|
);
|
|
|
|
// If not a valid post type, return false.
|
|
if ( ! \in_array( $post_after->post_type, $ld_builder_post_types, true ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Return early if author is not the same.
|
|
if ( (int) $post_before->post_author !== (int) $post_after->post_author ) {
|
|
return false;
|
|
}
|
|
|
|
$post_type = $post_after->post_type;
|
|
|
|
// Check if the post title changed from the default one set by the builder to the one specified by the user.
|
|
if ( $default_post_title_from_builder[ $post_type ] === $post_before->post_title && $default_post_title_from_builder[ $post_type ] !== $post_after->post_title ) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Get Learndash start and end dates from post meta, for courses and groups.
|
|
*
|
|
* @param int $post_id - Post ID.
|
|
* @param string $post_type - Post type ('sfwd-courses' or 'groups').
|
|
*
|
|
* @return array - Array with 'start_date' and 'end_date' keys.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_start_end_dates_meta( $post_id, $post_type ): array {
|
|
|
|
$meta_key = '';
|
|
$start_date_key = '';
|
|
$end_date_key = '';
|
|
|
|
$dates = array(
|
|
'start_date' => null,
|
|
'end_date' => null,
|
|
);
|
|
|
|
// Return early if post type is not supported.
|
|
if ( ! in_array( $post_type, array( 'sfwd-courses', 'groups' ), true ) ) {
|
|
return $dates;
|
|
}
|
|
|
|
// Determine meta key and date field keys based on post type.
|
|
if ( 'sfwd-courses' === $post_type ) {
|
|
$meta_key = '_sfwd-courses';
|
|
$start_date_key = 'sfwd-courses_course_start_date';
|
|
$end_date_key = 'sfwd-courses_course_end_date';
|
|
} elseif ( 'groups' === $post_type ) {
|
|
$meta_key = '_groups';
|
|
$start_date_key = 'groups_group_start_date';
|
|
$end_date_key = 'groups_group_end_date';
|
|
}
|
|
|
|
// Get current dates from database (before any updates).
|
|
$current_meta = \get_post_meta( $post_id, $meta_key, true );
|
|
$unserialized_meta = \maybe_unserialize( $current_meta );
|
|
|
|
if ( \is_array( $unserialized_meta ) ) {
|
|
$start_date = $unserialized_meta[ $start_date_key ] ?? null;
|
|
$end_date = $unserialized_meta[ $end_date_key ] ?? null;
|
|
|
|
$dates = array(
|
|
'start_date' => $start_date,
|
|
'end_date' => $end_date,
|
|
);
|
|
}
|
|
|
|
return $dates;
|
|
}
|
|
|
|
/**
|
|
* Get a comma-separated string of course titles enrolled in a group.
|
|
*
|
|
* @param int $group_id - Group ID.
|
|
*
|
|
* @return string - Comma-separated course titles or 'None' if no courses.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_courses_string_by_group_id( $group_id ) {
|
|
$available_courses = self::get_ld_none_string();
|
|
|
|
if ( \function_exists( 'learndash_group_enrolled_courses' ) ) {
|
|
$course_ids = \learndash_group_enrolled_courses( $group_id );
|
|
|
|
if ( ! empty( $course_ids ) && \is_array( $course_ids ) ) {
|
|
$course_titles = self::get_post_titles_array( $course_ids );
|
|
$available_courses = \implode( ', ', $course_titles );
|
|
}
|
|
}
|
|
|
|
return $available_courses;
|
|
}
|
|
|
|
/**
|
|
* Get the parent lesson ID for a topic.
|
|
*
|
|
* @param int $topic_id - Topic post ID.
|
|
*
|
|
* @return int|null - Lesson ID or null if not found.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_topic_lesson_id( $topic_id ) {
|
|
if ( empty( $topic_id ) ) {
|
|
return null;
|
|
}
|
|
|
|
// LearnDash stores the parent lesson ID in post meta.
|
|
$lesson_id = \get_post_meta( $topic_id, 'lesson_id', true );
|
|
|
|
if ( ! empty( $lesson_id ) ) {
|
|
return (int) $lesson_id;
|
|
}
|
|
|
|
// Fallback: try using LearnDash function if available.
|
|
if ( \function_exists( 'learndash_get_lesson_id' ) ) {
|
|
$ld_lesson_id = \learndash_get_lesson_id( $topic_id );
|
|
if ( ! empty( $ld_lesson_id ) ) {
|
|
return (int) $ld_lesson_id;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Get a comma-separated string of group titles for a course.
|
|
*
|
|
* @param int $course_id - Course ID.
|
|
*
|
|
* @return string - Comma-separated group titles or 'None' if no groups.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_post_groups_string( $course_id ) {
|
|
if ( empty( $course_id ) ) {
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
$group_ids = array();
|
|
|
|
// Get all groups that have this course enrolled.
|
|
if ( \function_exists( 'learndash_get_course_groups' ) ) {
|
|
$group_ids = \learndash_get_course_groups( $course_id, true );
|
|
}
|
|
|
|
if ( empty( $group_ids ) || ! \is_array( $group_ids ) ) {
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
$group_titles = self::get_post_titles_array( $group_ids );
|
|
|
|
if ( ! empty( $group_titles ) ) {
|
|
return \implode( ', ', $group_titles );
|
|
}
|
|
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
/**
|
|
* Get LearnDash transaction price amount with currency.
|
|
*
|
|
* @param int $transaction_id - Transaction post ID.
|
|
*
|
|
* @return string - Price amount with currency prefix (e.g., 'USD 99.00') or empty string.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_price_amount_with_currency( $transaction_id ) {
|
|
$pricing_info_meta = \get_post_meta( $transaction_id, 'pricing_info', true );
|
|
$pricing_info = \maybe_unserialize( $pricing_info_meta );
|
|
|
|
$price_amount = '';
|
|
$currency = '';
|
|
|
|
if ( \is_array( $pricing_info ) ) {
|
|
$price_amount = $pricing_info['price'] ?? '';
|
|
$currency = $pricing_info['currency'] ?? '';
|
|
}
|
|
|
|
if ( ! empty( $currency ) && ! empty( $price_amount ) ) {
|
|
$price_amount = $currency . ' ' . $price_amount;
|
|
}
|
|
|
|
return $price_amount;
|
|
}
|
|
|
|
/**
|
|
* Get the admin edit link for a LearnDash transaction post.
|
|
*
|
|
* @param \WP_Post $transaction_post - Transaction post object.
|
|
*
|
|
* @return string - Admin URL to edit the transaction or its parent order.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_ld_tx_post_edit_link( $transaction_post ) {
|
|
$transaction_id = (int) $transaction_post->ID;
|
|
$parent_transaction_id = (int) $transaction_post->post_parent;
|
|
$order_edit_link = '';
|
|
|
|
if ( $parent_transaction_id > 0 ) {
|
|
$order_edit_link = \admin_url( 'post.php?post=' . $parent_transaction_id . '&action=edit' );
|
|
} else {
|
|
$order_edit_link = \admin_url( 'post.php?post=' . $transaction_id . '&action=edit' );
|
|
}
|
|
|
|
return $order_edit_link;
|
|
}
|
|
|
|
|
|
/**
|
|
* Format progress as a percentage string.
|
|
*
|
|
* @param int $completed - Number of completed items.
|
|
* @param int $total - Total number of items.
|
|
*
|
|
* @return string Progress percentage formatted as "X.X%".
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
private static function format_progress_percentage( $completed, $total ): string {
|
|
if ( $total > 0 ) {
|
|
$progress = ( $completed / $total ) * 100;
|
|
return \round( $progress, 1 ) . '%';
|
|
}
|
|
|
|
return '0%';
|
|
}
|
|
|
|
/**
|
|
* Calculate course progress percentage.
|
|
*
|
|
* @param int $user_id - User ID.
|
|
* @param int $course_id - Course ID.
|
|
*
|
|
* @return string Progress percentage.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function calculate_course_progress( $user_id, $course_id ): string {
|
|
if ( \function_exists( 'learndash_course_get_completed_steps' ) ) {
|
|
|
|
if ( 'sfwd-courses' === \get_post_type( $course_id ) ) {
|
|
$completed_steps = \learndash_course_get_completed_steps( $user_id, $course_id );
|
|
$total_steps = \learndash_get_course_steps_count( $course_id );
|
|
|
|
return self::format_progress_percentage( $completed_steps, $total_steps );
|
|
}
|
|
}
|
|
|
|
return '0%';
|
|
}
|
|
|
|
/**
|
|
* Calculate lesson progress percentage.
|
|
*
|
|
* @param int $user_id - User ID.
|
|
* @param int $lesson_id - Lesson ID.
|
|
*
|
|
* @return string Progress percentage.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function calculate_lesson_progress( $user_id, $lesson_id ) {
|
|
if ( \function_exists( 'learndash_get_topic_list' ) && \function_exists( 'learndash_is_topic_complete' ) ) {
|
|
// Get all topics for this lesson.
|
|
$topics = \learndash_get_topic_list( $lesson_id );
|
|
|
|
if ( ! empty( $topics ) && \is_array( $topics ) ) {
|
|
$total_topics = \count( $topics );
|
|
$completed_topics = 0;
|
|
|
|
// Count how many topics are completed.
|
|
foreach ( $topics as $topic ) {
|
|
if ( \learndash_is_topic_complete( $user_id, $topic->ID ) ) {
|
|
++$completed_topics;
|
|
}
|
|
}
|
|
|
|
return self::format_progress_percentage( $completed_topics, $total_topics );
|
|
}
|
|
}
|
|
|
|
return '0%';
|
|
}
|
|
|
|
/**
|
|
* Get quiz result (percentage) from the most recent quiz attempt.
|
|
*
|
|
* @param int $user_id - User ID.
|
|
* @param int $quiz_id - Quiz ID.
|
|
*
|
|
* @return string - Quiz result as percentage string (e.g., "85%") or empty string if not available.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_quiz_result( $user_id, $quiz_id ) {
|
|
$quiz_attempts = \get_user_meta( $user_id, '_sfwd-quizzes', true );
|
|
|
|
if ( empty( $quiz_attempts ) || ! \is_array( $quiz_attempts ) ) {
|
|
return '';
|
|
}
|
|
|
|
// Find the most recent attempt for this quiz.
|
|
$latest_attempt = null;
|
|
$latest_time = 0;
|
|
|
|
foreach ( $quiz_attempts as $attempt ) {
|
|
if ( isset( $attempt['quiz'] ) && (int) $attempt['quiz'] === (int) $quiz_id ) {
|
|
$attempt_time = isset( $attempt['time'] ) ? (int) $attempt['time'] : 0;
|
|
|
|
if ( $attempt_time > $latest_time ) {
|
|
$latest_time = $attempt_time;
|
|
$latest_attempt = $attempt;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ( null === $latest_attempt || ! isset( $latest_attempt['percentage'] ) ) {
|
|
return '';
|
|
}
|
|
|
|
$result = round( $latest_attempt['percentage'], 2 ) . '%';
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Get quiz type (release schedule) label.
|
|
*
|
|
* @param int $quiz_id - Quiz ID.
|
|
*
|
|
* @return string - Quiz type label: "Immediately", "Enrollment-based", "Specific date", or empty string.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_quiz_type( $quiz_id ) {
|
|
$lesson_schedule = \learndash_get_setting( $quiz_id, 'lesson_schedule' );
|
|
|
|
$value = '';
|
|
|
|
if ( empty( $lesson_schedule ) ) {
|
|
$value = \esc_html__( 'Immediately', 'wp-security-audit-log' );
|
|
}
|
|
|
|
if ( 'visible_after' === $lesson_schedule ) {
|
|
$value = \esc_html__( 'Enrollment-based', 'wp-security-audit-log' );
|
|
}
|
|
|
|
if ( 'visible_after_specific_date' === $lesson_schedule ) {
|
|
$value = \esc_html__( 'Specific date', 'wp-security-audit-log' );
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* Get user's course groups as comma-separated string.
|
|
*
|
|
* @param int $user_id - User ID.
|
|
* @param int $course_id - Course ID.
|
|
*
|
|
* @return string Comma-separated group names or 'None'.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_user_course_groups( $user_id, $course_id ) {
|
|
if ( ! $course_id ) {
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
if ( ! \function_exists( 'learndash_get_users_group_ids' ) || ! \function_exists( 'learndash_group_enrolled_courses' ) ) {
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
// Get all groups the user is in.
|
|
$user_groups = \learndash_get_users_group_ids( $user_id );
|
|
|
|
if ( empty( $user_groups ) ) {
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
// Filter groups that have this course.
|
|
$course_groups = array();
|
|
|
|
foreach ( $user_groups as $group_id ) {
|
|
$group_courses = \learndash_group_enrolled_courses( $group_id );
|
|
if ( \in_array( $course_id, $group_courses, true ) ) {
|
|
$course_groups[] = \get_the_title( $group_id );
|
|
}
|
|
}
|
|
|
|
if ( empty( $course_groups ) ) {
|
|
return self::get_ld_none_string();
|
|
}
|
|
|
|
return \implode( ', ', $course_groups );
|
|
}
|
|
|
|
/**
|
|
* Build event data arrays for all quiz answer submissions.
|
|
*
|
|
* @param object $quiz_attempt - Quiz attempt record from wp_learndash_pro_quiz_statistic_ref table.
|
|
* @param array $answers - Array of answer records from wp_learndash_pro_quiz_statistic table.
|
|
*
|
|
* @return array Array of event data arrays, one for each answer.
|
|
*
|
|
* @since 5.6.0
|
|
*/
|
|
public static function get_quiz_answers_event_data( $quiz_attempt, $answers ): array {
|
|
if ( empty( $answers ) || ! $quiz_attempt ) {
|
|
return array();
|
|
}
|
|
|
|
$user_id = (int) $quiz_attempt->user_id;
|
|
$quiz_post_id = (int) $quiz_attempt->quiz_post_id;
|
|
$course_id = (int) $quiz_attempt->course_post_id;
|
|
|
|
$user = \get_user_by( 'ID', $user_id );
|
|
|
|
if ( ! $user ) {
|
|
return array();
|
|
}
|
|
|
|
$first_name = \get_user_meta( $user_id, 'first_name', true );
|
|
$last_name = \get_user_meta( $user_id, 'last_name', true );
|
|
|
|
$quiz_title = \get_the_title( $quiz_post_id );
|
|
$course_title = $course_id ? \get_the_title( $course_id ) : '';
|
|
$course_groups = self::get_user_course_groups( $user_id, $course_id );
|
|
|
|
$price_type = '';
|
|
|
|
if ( $course_id ) {
|
|
$course_meta = \get_post_meta( $course_id, '_sfwd-courses', true );
|
|
$price_type = $course_meta['sfwd-courses_course_price_type'] ?? '';
|
|
}
|
|
|
|
// Calculate quiz totals.
|
|
$total_questions = \count( $answers );
|
|
$correct_count = 0;
|
|
$total_points = 0;
|
|
|
|
foreach ( $answers as $answer ) {
|
|
$correct_count += (int) $answer->correct_count;
|
|
$total_points += (float) $answer->points;
|
|
}
|
|
|
|
// Build event data for each answer.
|
|
$events_data = array();
|
|
|
|
foreach ( $answers as $index => $answer ) {
|
|
$question_number = $index + 1;
|
|
$question_title = \get_the_title( $answer->question_post_id );
|
|
$is_correct = (int) $answer->correct_count === 1;
|
|
$points_earned = (float) $answer->points;
|
|
$quiz_progress = self::format_progress_percentage( $question_number, $total_questions );
|
|
|
|
$events_data[] = array(
|
|
'TargetUsername' => $user->user_login,
|
|
'FirstName' => $first_name,
|
|
'LastName' => $last_name,
|
|
'QuizTitle' => $quiz_title,
|
|
'QuizProgress' => $quiz_progress,
|
|
'CourseGroup' => $course_groups,
|
|
'PriceType' => $price_type,
|
|
'CourseTitle' => $course_title,
|
|
'QuestionTitle' => $question_title,
|
|
'IsCorrect' => $is_correct ? 'Yes' : 'No',
|
|
'PointsEarned' => $points_earned,
|
|
'TotalQuestions' => $total_questions,
|
|
'CorrectAnswers' => $correct_count,
|
|
'TotalScore' => $total_points,
|
|
);
|
|
}
|
|
|
|
return $events_data;
|
|
}
|
|
|
|
/**
|
|
* Builds the event variables array for LearnDash view/open events.
|
|
*
|
|
* @param \WP_Post $post - Post object.
|
|
*
|
|
* @return array
|
|
*
|
|
* @since 5.6.2
|
|
*/
|
|
public static function build_ld_view_event_variables( $post ): array {
|
|
$author_name = self::get_author_display_name( $post );
|
|
|
|
$variables = array(
|
|
'PostTitle' => \esc_html( $post->post_title ),
|
|
'PostID' => $post->ID,
|
|
'PostStatus' => $post->post_status,
|
|
'Category' => self::get_post_categories( $post->ID ),
|
|
);
|
|
|
|
if ( 'sfwd-courses' === $post->post_type ) {
|
|
$variables['CourseAuthor'] = $author_name;
|
|
$variables['CourseCategory'] = self::get_ld_post_categories( $post->ID, $post->post_type );
|
|
} elseif ( 'sfwd-lessons' === $post->post_type ) {
|
|
$variables['LessonAuthor'] = $author_name;
|
|
$variables['LessonCategory'] = self::get_ld_post_categories( $post->ID, $post->post_type );
|
|
}
|
|
|
|
return $variables;
|
|
}
|
|
}
|
|
}
|