| Current Path : /home/ereika83/www/wp-content/plugins/sfwd-lms/includes/classes/ |
| Current File : /home/ereika83/www/wp-content/plugins/sfwd-lms/includes/classes/class-ldlms-model-course-steps.php |
<?php
/**
* LearnDash Course Steps Class.
*
* @since 2.5.0
* @package LearnDash\Course\Steps
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ( ! class_exists( 'LDLMS_Course_Steps' ) ) && ( class_exists( 'LDLMS_Model' ) ) ) {
/**
* Class for LearnDash Course Steps.
*
* @since 2.5.0
* @uses LDLMS_Model
*/
class LDLMS_Course_Steps extends LDLMS_Model {
/**
* Course ID for use in this instance.
*
* @var integer $course_id
*/
private $course_id = 0;
/**
* Course Steps Loaded flag.
*
* @var boolean $steps_loaded Set to false initially. Set to true once course
* steps have been loaded.
*/
private $steps_loaded = false;
/**
* Course Steps are dirty.
*
* @var boolean $steps_dirty Set to false initially but can be set to true if the
* dirty meta is read in and it true.
*/
private $steps_dirty = false;
/**
* Course Steps being saved.
*
* @since 3.4.2
*
* @var boolean $saving_steps Set to (bool) true when the steps are being saved.
*/
private $saving_steps = false;
/**
* Steps meta array.
*
* @since 3.4.0
*
* @var array $meta Array of post meta details including steps.
*/
protected $meta = array();
/**
* Course Steps array.
*
* @var array $steps Array of course steps.
*/
protected $steps = array();
/**
* Course Steps Objects Loaded flag.
*
* @var boolean $objects_loaded Set to false initially. Set to true once course
* steps objects have been loaded.
*/
protected $objects_loaded = false;
/**
* Course Objects array.
*
* @var array $objects Array of course steps.
*/
protected $objects = array();
/**
* Course post types
*
* @var array $steps_post_types Course post types.
*/
protected $steps_post_types = array();
/**
* Public constructor for class.
*
* @since 2.6.0
*
* @param integer $course_id Course post ID.
*/
public function __construct( $course_id = 0 ) {
if ( ! empty( $course_id ) ) {
$this->course_id = absint( $course_id );
$this->steps_post_types = learndash_get_post_types( 'course_steps' );
}
}
/**
* Load Course Steps.
*
* @since 2.6.0
*/
public function load_steps() {
if ( ! $this->steps_loaded ) {
$this->steps_loaded = true;
$this->load_steps_meta();
if ( true === $this->meta['empty'] ) {
// Note here since we are loading the steps via legacy methods we don't need to validate.
$this->steps['h'] = $this->load_steps_legacy();
// I don't want to affect the default flow, so this is a safe way to do this step after it was imported only.
if ( get_post_meta( $this->course_id, 'course_steps_update_after_import_is_needed', true ) ) {
// This is necessary to rebuild the steps after the import.
$steps_h = $this->steps['h'];
delete_post_meta( $this->course_id, 'course_steps_update_after_import_is_needed' );
}
}
$this->build_steps();
if ( $this->is_steps_dirty() ) {
if ( true === $this->meta['course_shared_steps_enabled'] ) {
$steps_h = $this->validate_steps( $this->steps['h'] );
} elseif ( true === $this->meta['course_builder_enabled'] ) {
$steps_h = $this->load_steps_legacy();
}
if ( ( ! isset( $steps_h ) ) || ( ! is_array( $steps_h ) ) ) {
$steps_h = array();
}
if ( wp_json_encode( $this->steps['h'] ) !== wp_json_encode( $steps_h ) ) {
$this->steps = array();
$this->steps['h'] = $steps_h;
$this->build_steps();
$this->save_steps_meta();
}
if ( $this->is_steps_dirty() ) {
$this->clear_steps_dirty();
}
} elseif ( true === $this->meta['empty'] ) {
// Finally clear our empty flag.
$this->meta['empty'] = false;
$this->save_steps_meta();
}
}
}
/**
* Load the Steps Post meta data set.
*
* @since 3.4.0
*/
protected function load_steps_meta() {
$save_after_load = false;
$this->objects_loaded = false;
$this->objects = array();
if ( learndash_is_course_builder_enabled() ) {
$this->meta = get_post_meta( $this->course_id, 'ld_course_steps', true );
}
if ( ( isset( $this->meta['h'] ) ) && ( ! isset( $this->meta['steps']['h'] ) ) ) {
$steps_h = $this->meta['h'];
$this->meta = array();
$this->meta['steps'] = array(
'h' => $steps_h,
);
$save_after_load = true;
}
if ( ! is_array( $this->meta ) ) {
$this->meta = array();
}
if ( ! isset( $this->meta['steps'] ) ) {
$this->meta['steps'] = array();
}
/**
* We check the 'course_id' to verify the step metadata. This helps
* with clone plugins that will copy the post_meta. In theory.
*/
if ( isset( $this->meta['course_id'] ) ) {
$this->meta['course_id'] = absint( $this->meta['course_id'] );
if ( $this->course_id !== $this->meta['course_id'] ) {
$this->meta['steps'] = array();
$save_after_load = true;
}
}
if ( ( isset( $this->meta['course_builder_enabled'] ) ) && ( true === (bool) $this->meta['course_builder_enabled'] ) ) {
if ( ! (bool) learndash_is_course_builder_enabled() ) {
$this->meta['steps'] = array();
$save_after_load = true;
}
}
if ( ( isset( $this->meta['course_shared_steps_enabled'] ) ) && ( true === (bool) $this->meta['course_shared_steps_enabled'] ) ) {
if ( ! (bool) learndash_is_course_shared_steps_enabled() ) {
$this->meta['steps'] = array();
$save_after_load = true;
}
}
$this->meta['course_id'] = $this->course_id;
$this->meta['version'] = LEARNDASH_VERSION;
if ( ( ! isset( $this->meta['steps']['h'] ) ) || ( ! is_array( $this->meta['steps']['h'] ) ) ) {
$this->meta['steps']['h'] = array();
$this->meta['empty'] = true;
} else {
$this->meta['empty'] = false;
}
$this->meta['course_builder_enabled'] = (bool) learndash_is_course_builder_enabled();
$this->meta['course_shared_steps_enabled'] = (bool) learndash_is_course_shared_steps_enabled();
if ( isset( $this->meta['steps'] ) ) {
$this->steps = $this->meta['steps'];
}
if ( true === $save_after_load ) {
$this->save_steps_meta();
}
}
/**
* Set the Steps Post meta data set.
*
* @since 3.4.0
*/
protected function save_steps_meta() {
if ( learndash_is_course_builder_enabled() ) {
if ( isset( $this->steps['h'] ) ) {
$this->meta['steps_count'] = $this->calculate_steps_count();
$this->meta['steps'] = array(
'h' => $this->steps['h'],
);
}
update_post_meta( $this->course_id, 'ld_course_steps', $this->meta );
} else {
delete_post_meta( $this->course_id, 'ld_course_steps' );
}
$this->set_steps_count_meta();
}
/**
* Sets the Course steps dirty flag and will force the steps to be
* reloaded from queries.
*
* @since 2.5.0
*/
public function set_steps_dirty() {
if ( ( ! empty( $this->course_id ) ) && ( true !== $this->saving_steps ) ) {
$this->steps_dirty = true;
return update_post_meta( $this->course_id, 'ld_course_steps_dirty', $this->course_id );
}
}
/**
* Check if the steps dirty flag is set.
*
* @since 2.5.0
*/
public function is_steps_dirty() {
// If the steps_dirty boolean has been previously set to try it save a call to postmeta.
if ( false === $this->steps_dirty ) {
if ( ! empty( $this->course_id ) ) {
$is_dirty = get_post_meta( $this->course_id, 'ld_course_steps_dirty', true );
if ( absint( $is_dirty ) === absint( $this->course_id ) ) {
$this->steps_dirty = true;
}
}
}
return $this->steps_dirty;
}
/**
* Clear the steps dirty flag.
*
* @since 2.5.0
*/
public function clear_steps_dirty() {
if ( ! empty( $this->course_id ) ) {
$this->steps_dirty = false;
delete_post_meta( $this->course_id, 'ld_course_steps_dirty' );
}
}
/**
* Get the total steps for the course.
*
* @since 2.6.0
*/
public function get_steps_count() {
$this->load_steps();
return $this->calculate_steps_count();
}
/**
* Calculate the total steps count for the course.
*
* @since 3.4.0
*/
protected function calculate_steps_count() {
$steps_count = 0;
if ( ( isset( $this->steps['h'] ) ) && ( ! empty( $this->steps['h'] ) ) ) {
if ( ! isset( $this->steps['t'] ) ) {
$this->steps['t'] = $this->steps_grouped_by_type( $this->steps['h'] );
}
// All Lessons.
if ( isset( $this->steps['t']['sfwd-lessons'] ) ) {
$steps_count += count( $this->steps['t']['sfwd-lessons'] );
}
// All topics.
if ( isset( $this->steps['t']['sfwd-topic'] ) ) {
$steps_count += count( $this->steps['t']['sfwd-topic'] );
}
// Any Global Quizzes only count as 1.
if ( ( isset( $this->steps['h']['sfwd-quiz'] ) ) && ( ! empty( $this->steps['h']['sfwd-quiz'] ) ) ) {
$steps_count++;
}
}
return $steps_count;
}
/**
* Set the total steps postmeta for the course.
*
* @since 3.4.0
*/
public function set_steps_count_meta() {
$steps_count = $this->calculate_steps_count();
update_post_meta( $this->course_id, '_ld_course_steps_count', $steps_count );
}
/**
* Build Course Steps nodes.
*
* @since 2.5.0
*/
protected function build_steps() {
if ( ! isset( $this->steps['h'] ) ) {
$this->steps['h'] = array();
}
if ( ! isset( $this->steps['t'] ) ) {
$this->steps['t'] = array();
}
if ( ! isset( $this->steps['r'] ) ) {
$this->steps['r'] = array();
}
if ( ! isset( $this->steps['l'] ) ) {
$this->steps['l'] = array();
}
if ( ! isset( $this->steps['co'] ) ) {
$this->steps['co'] = array();
}
if ( ! isset( $this->steps['legacy'] ) ) {
$this->steps['legacy'] = array();
}
if ( ! isset( $this->steps['sections'] ) ) {
$this->steps['sections'] = array();
}
if ( isset( $this->steps['h']['section-heading'] ) ) {
unset( $this->steps['h']['section-heading'] );
}
if ( ! empty( $this->steps['h'] ) ) {
if ( empty( $this->steps['t'] ) ) {
$this->steps['t'] = $this->steps_grouped_by_type( $this->steps['h'] );
}
if ( empty( $this->steps['l'] ) ) {
$this->steps['l'] = $this->steps_grouped_linear( $this->steps['h'] );
}
if ( empty( $this->steps['co'] ) ) {
$this->steps['co'] = $this->steps_grouped_completion_order( $this->steps['h'] );
}
if ( empty( $this->steps['r'] ) ) {
$this->steps['r'] = $this->steps_grouped_reverse_keys( $this->steps['h'] );
}
if ( empty( $this->steps['sections'] ) ) {
$this->steps['sections'] = $this->steps_grouped_sections( $this->steps['h'] );
}
if ( empty( $this->steps['legacy'] ) ) {
$this->steps['legacy'] = $this->steps_grouped_legacy( $this->steps['h'] );
if ( ! isset( $this->steps['legacy']['lessons'] ) ) {
$this->steps['legacy']['lessons'] = array();
}
if ( ! isset( $this->steps['legacy']['topics'] ) ) {
$this->steps['legacy']['topics'] = array();
}
}
$this->load_steps_objects();
}
}
/**
* Validate Course Steps nodes and items.
*
* @since 2.5.0
*
* @param array $steps Current steps nodes and items.
*
* @return array $steps Valid steps nodes and items.
*/
protected function validate_steps( $steps = array() ) {
if ( ! empty( $steps ) ) {
foreach ( $steps as $steps_type => $steps_type_set ) {
if ( ( is_array( $steps_type_set ) ) && ( ! empty( $steps_type_set ) ) ) {
foreach ( $steps_type_set as $steps_type_id => $steps_type_items ) {
if ( ! isset( $this->objects[ $steps_type_id ] ) ) {
unset( $steps[ $steps_type ][ $steps_type_id ] );
} else {
$steps[ $steps_type ][ $steps_type_id ] = $this->validate_steps( $steps_type_items );
}
}
}
}
}
return $steps;
}
/**
* This converts the normal hierarchy steps into an array groups be the post type. This is easier for search.
*
* @since 2.5.0
*
* @param array $steps Array of Course steps nodes and items.
*
* @return array Array of steps by type.
*/
protected function steps_grouped_by_type( $steps = array() ) {
$steps_by_type = array();
if ( ! empty( $steps ) ) {
foreach ( $steps as $steps_type => $steps_type_set ) {
if ( ! isset( $steps_by_type[ $steps_type ] ) ) {
$steps_by_type[ $steps_type ] = array();
}
if ( ( is_array( $steps_type_set ) ) && ( ! empty( $steps_type_set ) ) ) {
foreach ( $steps_type_set as $step_id => $step_id_set ) {
$steps_by_type[ $steps_type ][] = $step_id;
if ( ( is_array( $step_id_set ) ) && ( ! empty( $step_id_set ) ) ) {
$sub_steps = $this->steps_grouped_by_type( $step_id_set );
if ( ! empty( $sub_steps ) ) {
foreach ( $sub_steps as $sub_step_type => $sub_step_ids ) {
if ( ! isset( $steps_by_type[ $sub_step_type ] ) ) {
$steps_by_type[ $sub_step_type ] = array();
}
if ( ! empty( $sub_step_ids ) ) {
$steps_by_type[ $sub_step_type ] = array_merge( $steps_by_type[ $sub_step_type ], $sub_step_ids );
}
}
}
}
}
}
}
}
return $steps_by_type;
}
/**
* Steps grouped linear.
*
* @since 2.5.0
*
* @param array $steps Array of Course step nodes and items.
*
* @return array Array of steps by linear.
*/
protected function steps_grouped_linear( $steps = array() ) {
$steps_linear = array();
if ( ! empty( $steps ) ) {
foreach ( $steps as $steps_type => $steps_type_set ) {
if ( ( is_array( $steps_type_set ) ) && ( ! empty( $steps_type_set ) ) ) {
foreach ( $steps_type_set as $step_id => $step_id_set ) {
$steps_linear[] = $steps_type . ':' . $step_id;
if ( ( is_array( $step_id_set ) ) && ( ! empty( $step_id_set ) ) ) {
$sub_steps = $this->steps_grouped_linear( $step_id_set );
if ( ! empty( $sub_steps ) ) {
$steps_linear = array_merge( $steps_linear, $sub_steps );
}
}
}
}
}
}
return $steps_linear;
}
/**
* Steps grouped Legacy.
*
* @since 3.4.0
*
* @param array $steps Array of Course step nodes and items.
*
* @return array Array of steps by legacy.
*/
protected function steps_grouped_legacy( $steps = array() ) {
$steps_legacy = array();
if ( ! empty( $steps ) ) {
foreach ( $steps as $steps_type => $steps_type_set ) {
if ( ( is_array( $steps_type_set ) ) && ( ! empty( $steps_type_set ) ) ) {
foreach ( $steps_type_set as $step_id => $step_id_set ) {
if ( learndash_get_post_type_slug( 'lesson' ) === $steps_type ) {
if ( ! isset( $steps_legacy['lessons'] ) ) {
$steps_legacy['lessons'] = array();
}
$steps_legacy['lessons'][ $step_id ] = 0;
if ( ! isset( $steps_legacy['topics'] ) ) {
$steps_legacy['topics'] = array();
}
$steps_legacy['topics'][ $step_id ] = $this->steps_grouped_legacy( $step_id_set );
if ( ! isset( $steps_legacy['total'] ) ) {
$steps_legacy['total'] = $this->calculate_steps_count();
}
} elseif ( learndash_get_post_type_slug( 'topic' ) === $steps_type ) {
$steps_legacy[ $step_id ] = 0;
}
}
}
}
}
return $steps_legacy;
}
/**
* Steps grouped by Completion Order.
*
* @since 3.4.0
*
* @param array $steps Array of Course step nodes and items.
*
* @return array Array of steps by complete order.
*/
protected function steps_grouped_completion_order( $steps = array() ) {
$steps_co = array();
if ( ! empty( $steps ) ) {
foreach ( $steps as $steps_type => $steps_type_set ) {
if ( ( is_array( $steps_type_set ) ) && ( ! empty( $steps_type_set ) ) ) {
foreach ( $steps_type_set as $step_id => $step_id_set ) {
if ( ( is_array( $step_id_set ) ) && ( ! empty( $step_id_set ) ) ) {
$sub_steps = $this->steps_grouped_completion_order( $step_id_set );
if ( ! empty( $sub_steps ) ) {
$steps_co = array_merge( $steps_co, $sub_steps );
}
}
$steps_co[] = $steps_type . ':' . $step_id;
}
}
}
}
return $steps_co;
}
/**
* Group Steps by Section.
*
* @since 3.4.0
*
* @param array $steps Array of Course sections nodes and related lessons.
*
* @return array Array of sections.
*/
protected function steps_grouped_sections( $steps = array() ) {
$sections = array();
$sections_raw = get_post_meta( $this->course_id, 'course_sections', true );
if ( empty( $sections_raw ) ) {
return $sections;
}
$sections_array = json_decode( $sections_raw );
if ( empty( $sections_array ) || ( ! is_array( $sections_array ) ) ) {
return $sections;
}
if ( ( ! isset( $steps['sfwd-lessons'] ) ) || ( empty( $steps['sfwd-lessons'] ) ) ) {
return $sections;
}
$lessons = array_keys( $steps['sfwd-lessons'] );
foreach ( $sections_array as $section ) {
if ( ! is_object( $section ) ) {
continue;
}
if ( ( ! property_exists( $section, 'ID' ) ) || ( empty( $section->ID ) ) ) {
continue;
}
if ( ! property_exists( $section, 'order' ) ) {
continue;
}
if ( ( ! property_exists( $section, 'post_title' ) ) || ( empty( $section->post_title ) ) ) {
continue;
}
if ( ( ! property_exists( $section, 'type' ) ) || ( empty( $section->type ) ) ) {
continue;
}
array_splice( $lessons, (int) $section->order, 0, array( $section ) );
}
foreach ( $lessons as $lesson ) {
if ( is_object( $lesson ) ) {
unset( $lesson->url );
unset( $lesson->edit_link );
unset( $lesson->tree );
unset( $lesson->expanded );
$lesson->steps = array();
$sections[] = $lesson;
} else {
if ( count( $sections ) ) {
$sections[ count( $sections ) - 1 ]->steps[] = $lesson;
}
}
}
return $sections;
}
/**
* Load the Steps Objects.
*
* This function will take the steps by type and load all needed
* WP_Post objects.
*
* @since 3.4.0
*/
protected function load_steps_objects() {
$all_objects_ids = array();
if ( $this->objects_loaded ) {
return;
}
if ( true === $this->saving_steps ) {
return;
}
$all_steps_ids = $this->get_all_steps_ids();
$this->objects = array();
if ( true !== $this->meta['course_shared_steps_enabled'] ) {
$steps_query_args = array(
'post_type' => $this->steps_post_types,
'suppress_filters' => 1,
'posts_per_page' => -1,
'post_status' => $this->get_step_post_statuses(),
'meta_query' => array(
array(
'key' => 'course_id',
'value' => absint( $this->course_id ),
'compare' => '=',
),
),
);
} else {
if ( empty( $all_steps_ids ) ) {
$this->objects_loaded = true;
return;
}
$steps_query_args = array(
'post_type' => $this->steps_post_types,
'suppress_filters' => 1,
'posts_per_page' => -1,
'post_status' => $this->get_step_post_statuses(),
'post__in' => $all_steps_ids,
);
}
/**
* Filters the $steps_query_args array use for Course Steps Objects Queries.
*
* @since 3.4.2
*
* @param array $steps_query_args Array of query args.
* @param int $course_id Course ID.
*/
$steps_query_args = apply_filters( 'learndash_course_steps_objects_query_args', $steps_query_args, $this->course_id );
if ( ( is_array( $steps_query_args ) ) && ( ! empty( $steps_query_args ) ) ) {
$steps_query = new WP_Query( $steps_query_args );
if ( ( $steps_query ) && ( is_a( $steps_query, 'WP_Query' ) ) ) {
foreach ( $steps_query->posts as $steps_post ) {
$this->objects[ $steps_post->ID ] = $steps_post;
}
$this->objects_loaded = true;
}
}
$all_objects_ids = $this->get_objects_steps_ids();
/**
* If we have loaded some objects we filter through these and remove
* the post IDs from all_steps to cut down on queried objects.
*/
if ( ( count( $all_objects_ids ) !== count( $all_steps_ids ) ) ) {
/**
* If we are not using Shared Steps we set the dirty
* flag and abort. This will cause the process to restart.
*/
if ( true !== $this->meta['course_shared_steps_enabled'] ) {
/**
* If here we have a mismatch of the number of steps in the structure
* vs. the number of objects queried. So we try and reconcile. Otherwise
* this causes the 'dirty' logic to be triggered over and over.
* See LEARNDASH-6721 for example.
*/
$all_intersect_ids = array_intersect( $all_steps_ids, $all_objects_ids );
if ( count( $all_intersect_ids ) ) {
$all_diff_ids = array_diff( $all_objects_ids, $all_intersect_ids );
if ( ! empty( $all_diff_ids ) ) {
foreach ( $all_diff_ids as $all_diff_id ) {
if ( ( isset( $this->objects[ $all_diff_id ] ) ) && ( is_a( $this->objects[ $all_diff_id ], 'WP_Post' ) ) ) {
$diff_post = $this->objects[ $all_diff_id ];
/**
* We only handle 'topic' items here because a topic needs both the
* 'course' and 'lesson' post meta. If a lesson or quiz is missing
* the 'course' it will auto-reconcile via the 'dirty' processing.
*/
if ( learndash_get_post_type_slug( 'topic' ) === $diff_post->post_type ) {
$valid_post = true;
/**
* We need to check the post_meta as well as the settings since they are
* stored separately. The post_meta can be changed outside of LD logic.
*/
$course_id_post_meta = (int) get_post_meta( $diff_post->ID, 'course_id', true );
$course_id_setting = (int) learndash_get_setting( $diff_post->ID, 'course' );
if ( ( empty( $course_id_post_meta ) ) || ( empty( $course_id_setting ) ) || ( $course_id_setting !== $course_id_post_meta ) ) {
$valid_post = false;
} else {
$lesson_id_post_meta = (int) get_post_meta( $diff_post->ID, 'lesson_id', true );
$lesson_id_setting = (int) learndash_get_setting( $diff_post->ID, 'lesson' );
if ( ( empty( $lesson_id_post_meta ) ) || ( empty( $lesson_id_setting ) ) || ( $lesson_id_setting !== $lesson_id_post_meta ) ) {
$valid_post = false;
}
}
// If we have an invalid post we clear the course and lesson references.
if ( true !== $valid_post ) {
learndash_update_setting( $diff_post->ID, 'course', 0 );
learndash_update_setting( $diff_post->ID, 'lesson', 0 );
}
}
}
}
}
}
$this->set_steps_dirty();
$this->objects = array();
$this->objects_loaded = false;
return;
}
/**
* The following code is left but legacy. If here then Share Course Steps
* is enabled. But since we are loading all the known steps object in the
* above WP_Query calls there should be no need to compare the arrays of ids.
*/
$all_intersect_ids = array_intersect( $all_steps_ids, $all_objects_ids );
// First remove the items we don't need.
$all_remove_ids = array_diff( $all_objects_ids, $all_intersect_ids );
if ( ! empty( $all_remove_ids ) ) {
foreach ( $all_remove_ids as $id_remove ) {
if ( isset( $this->objects[ $id_remove ] ) ) {
unset( $this->objects[ $id_remove ] );
}
}
}
// Then add the new items.
$all_add_ids = array_diff( $all_steps_ids, $all_intersect_ids );
if ( ! empty( $all_add_ids ) ) {
$all_steps_chunks_ids = array_chunk( $all_add_ids, LEARNDASH_LMS_COURSE_STEPS_LOAD_BATCH_SIZE );
foreach ( $all_steps_chunks_ids as $steps_chunk_ids ) {
if ( empty( $steps_chunk_ids ) ) {
continue;
}
$steps_query_args = array(
'post_type' => $this->steps_post_types,
'posts_per_page' => -1,
'post_status' => $this->get_step_post_statuses(),
'post__in' => $steps_chunk_ids,
);
$steps_query = new WP_Query( $steps_query_args );
if ( ( is_a( $steps_query, 'WP_Query' ) ) && ( property_exists( $steps_query, 'posts' ) ) && ( ! empty( $steps_query->posts ) ) ) {
foreach ( $steps_query->posts as $steps_post ) {
$this->objects[ $steps_post->ID ] = $steps_post;
}
}
}
}
}
}
/**
* Get all steps ids
*
* @since 3.4.0.7
*/
protected function get_all_steps_ids() {
$all_steps_ids = array();
$steps_by_type = array();
if ( isset( $this->steps['t'] ) ) {
$steps_by_type = $this->steps['t'];
} else {
if ( isset( $this->steps['h'] ) ) {
$steps_by_type = $this->steps_grouped_by_type( $this->steps['h'] );
}
}
if ( ! empty( $steps_by_type ) ) {
if ( isset( $steps_by_type['sfwd-lessons'] ) ) {
$all_steps_ids = array_merge( $all_steps_ids, $steps_by_type['sfwd-lessons'] );
}
if ( isset( $steps_by_type['sfwd-topic'] ) ) {
$all_steps_ids = array_merge( $all_steps_ids, $steps_by_type['sfwd-topic'] );
}
if ( isset( $steps_by_type['sfwd-quiz'] ) ) {
$all_steps_ids = array_merge( $all_steps_ids, $steps_by_type['sfwd-quiz'] );
}
}
if ( ! empty( $all_steps_ids ) ) {
$all_steps_ids = array_map( 'absint', $all_steps_ids );
}
return array_values( $all_steps_ids );
}
/**
* Get the post IDs of the objects.
*
* @since 3.4.0.7
*
* @return array Array of Post IDs
*/
protected function get_objects_steps_ids() {
$objects_steps_ids = array();
if ( ! empty( $this->objects ) ) {
$objects_steps_ids = wp_list_pluck( $this->objects, 'ID' );
}
if ( ! empty( $objects_steps_ids ) ) {
$objects_steps_ids = array_map( 'absint', $objects_steps_ids );
}
return array_values( $objects_steps_ids );
}
/**
* Group Steps reversed keys.
*
* @since 2.5.0
*
* @param array $steps Array of Course step nodes and items.
* @return array Array of steps.
*/
protected function steps_grouped_reverse_keys( $steps = array() ) {
$steps_reversed = $this->steps_reverse_keys_walk( $steps );
if ( ! empty( $steps_reversed ) ) {
foreach ( $steps_reversed as $reversed_key => $reversed_set ) {
if ( ! empty( $reversed_set ) ) {
$steps_reversed[ $reversed_key ] = $this->flatten_item_parent_steps( $reversed_set );
} else {
$steps_reversed[ $reversed_key ] = array();
}
}
}
return $steps_reversed;
}
/**
* Internal utility function to reverse walk the Course steps nodes and items
*
* @since 2.5.0
*
* @param array $steps Array of Course step nodes and items.
* @param array $parent_tree Patent array.
*
* @return array Array of steps.
*/
private function steps_reverse_keys_walk( $steps, $parent_tree = array() ) {
$steps_reversed = array();
if ( ! empty( $steps ) ) {
foreach ( $steps as $steps_type => $steps_type_set ) {
if ( ( is_array( $steps_type_set ) ) && ( ! empty( $steps_type_set ) ) ) {
foreach ( $steps_type_set as $step_id => $step_id_set ) {
$steps_parents = array();
$steps_parents[ $steps_type . ':' . $step_id ] = $parent_tree;
if ( ( is_array( $step_id_set ) ) && ( ! empty( $step_id_set ) ) ) {
$sub_steps = $this->steps_reverse_keys_walk( $step_id_set, $steps_parents );
if ( ! empty( $sub_steps ) ) {
$steps_parents = array_merge( $steps_parents, $sub_steps );
}
}
if ( ! empty( $steps_parents ) ) {
$steps_reversed = array_merge( $steps_reversed, $steps_parents );
}
}
}
}
}
return $steps_reversed;
}
/**
* Internal utility function to reverse parent keys of the Course nodes and items.
*
* @since 2.5.0
*
* @param array $steps Array of Course step nodes and items.
*
* @return array Array of steps.
*/
private function flatten_item_parent_steps( $steps = array() ) {
$flattened_steps = array();
if ( ! empty( $steps ) ) {
foreach ( $steps as $a_step_key => $a_steps ) {
$flattened_steps[] = $a_step_key;
$sub_steps = $this->flatten_item_parent_steps( $a_steps );
if ( ! empty( $sub_steps ) ) {
$flattened_steps = array_merge( $flattened_steps, $sub_steps );
}
}
}
return $flattened_steps;
}
/**
* Set Course steps.
* This is generally called when editing the course and the course steps has been changed.
*
* @since 2.5.0
* @since 4.10.0 Add $keep_sections parameter to keep sections when updating steps.
*
* @param array $course_steps Array of Course steps.
* @param bool $keep_sections Whether to keep the existing sections when setting steps or not. Default false.
*
* @return void
*/
public function set_steps( $course_steps = array(), bool $keep_sections = false ): void {
if ( ! empty( $this->course_id ) ) {
$this->saving_steps = true;
$this->load_steps_meta();
if ( $keep_sections ) {
// Get the value directly from meta data to keep the structure of the sections unchanged to maintain its function in the course builder.
$course_sections = get_post_meta( $this->course_id, 'course_sections', true );
if ( is_string( $course_sections ) ) {
$course_sections = json_decode( $course_sections, true );
}
$course_sections = is_array( $course_sections ) ? $course_sections : [];
$this->set_section_headings( $course_sections );
} else {
if ( isset( $course_steps['section-heading'] ) ) {
$this->set_section_headings( $course_steps['section-heading'] );
unset( $course_steps['section-heading'] );
} else {
$this->set_section_headings( array() );
}
}
$this->steps['h'] = $course_steps;
$this->build_steps();
if ( ! learndash_is_course_shared_steps_enabled() ) {
$this->set_step_to_course_legacy();
} else {
$this->set_step_to_course();
}
$this->save_steps_meta();
$this->set_steps_count_meta();
$this->saving_steps = false;
if ( $this->is_steps_dirty() ) {
$this->clear_steps_dirty();
}
$this->steps_loaded = false;
}
}
/**
* Set course steps while keeping the sections.
*
* @since 4.10.0
*
* @param array<string, array<mixed>> $course_steps Course steps.
*
* @return void
*/
public function set_steps_keeping_sections( array $course_steps = [] ): void {
$this->set_steps( $course_steps, true );
}
/**
* Set Course Section Headings.
*
* @since 3.5.0
*
* @param array $sections Array of Section headings.
*
* @return bool return from `update_post_status()`.
*/
public function set_section_headings( $sections = array() ) {
// This probably should call the REST endpoint.
if ( ! empty( $sections ) ) {
foreach ( $sections as &$section ) {
if ( ! isset( $section['post_title'] ) ) {
$section['post_title'] = '';
} elseif ( ! empty( $section['post_title'] ) ) {
$section['post_title'] = wp_strip_all_tags( $section['post_title'] );
}
}
$sections_json = wp_slash( wp_json_encode( array_values( $sections ), JSON_UNESCAPED_UNICODE ) );
return update_post_meta( $this->course_id, 'course_sections', $sections_json );
} else {
return delete_post_meta( $this->course_id, 'course_sections' );
}
}
/**
* Get Course steps by node type.
*
* @since 2.5.0
*
* @param string $steps_type Course Steps node type.
*
* @return array of Course Step items found in node.
*/
public function get_steps( $steps_type = 'h' ) {
$this->load_steps();
if ( isset( $this->steps[ $steps_type ] ) ) {
return $this->steps[ $steps_type ];
} elseif ( 'all' === $steps_type ) {
return $this->steps;
}
return array();
}
/**
* This function sets a post_meta association for the various steps within the course.
* The new association is 'ld_course_XXX' where 'XXX' is the course ID.
*
* @since 2.5.0
*/
public function set_step_to_course() {
global $wpdb;
$course_steps_new = array();
if ( ( isset( $this->steps['t'] ) ) && ( ! empty( $this->steps['t'] ) ) ) {
foreach ( $this->steps['t'] as $step_type => $step_type_set ) {
if ( ! empty( $step_type_set ) ) {
$course_steps_new = array_merge( $course_steps_new, $step_type_set );
}
}
}
if ( ! empty( $course_steps_new ) ) {
sort( $course_steps_new, SORT_NUMERIC );
}
$course_steps_old = $wpdb->get_col( $wpdb->prepare( 'SELECT post_id as post_id FROM ' . $wpdb->postmeta . ' WHERE meta_key = %s', 'ld_course_' . $this->course_id ) );
if ( ! empty( $course_steps_old ) ) {
sort( $course_steps_old, SORT_NUMERIC );
}
$course_steps_intersect = array_intersect( $course_steps_new, $course_steps_old );
// Add Steps.
$course_steps_add = array_diff( $course_steps_new, $course_steps_intersect );
if ( ! empty( $course_steps_add ) ) {
$course_steps_add_chunks = array_chunk( $course_steps_add, LEARNDASH_LMS_DEFAULT_CB_INSERT_CHUNK_SIZE );
foreach ( $course_steps_add_chunks as $insert_post_ids ) {
$insert_sql_str = '';
foreach ( $insert_post_ids as $post_id ) {
if ( ! empty( $insert_sql_str ) ) {
$insert_sql_str .= ',';
}
$insert_sql_str .= '(' . $post_id . ", 'ld_course_" . $this->course_id . "'," . $this->course_id . ')';
}
if ( ! empty( $insert_sql_str ) ) {
$insert_sql_str = 'INSERT INTO ' . $wpdb->postmeta . ' (`post_id`, `meta_key`, `meta_value`) VALUES ' . $insert_sql_str;
//phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
$wpdb->query( $insert_sql_str );
// phpcs:enable
}
}
}
// Remove Steps.
$course_steps_remove = array_diff( $course_steps_old, $course_steps_intersect );
if ( ! empty( $course_steps_remove ) ) {
$wpdb->query(
$wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN (" . LDLMS_DB::escape_IN_clause_placeholders( $course_steps_remove ) . ')', array_merge( array( 'ld_course_' . $this->course_id ), LDLMS_DB::escape_IN_clause_values( $course_steps_remove ) ) ) //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
);
}
/**
* Secondary processing here we need to determine all the primary associations for this course and remove any items no longer associated.
* For example prior to v2.5 you may have a course ID #123. The course has a lesson, topic and global quiz. Each of these items will have
* a post_meta reference 'course_id'. Now in v2.5 the course steps are stored into a collection or nodes. But if for example the quiz is
* remove we need to also remove the legacy 'course_id' association.
*/
$course_steps_primary = $wpdb->get_col(
$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
"SELECT posts.ID as post_id FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->postmeta} as postmeta ON posts.ID = postmeta.post_id WHERE 1=1 AND postmeta.meta_key = %s AND postmeta.meta_value = %d AND posts.post_type IN (" . LDLMS_DB::escape_IN_clause_placeholders( $this->steps_post_types ) . ')',
array_merge( array( 'course_id' ), array( $this->course_id ), LDLMS_DB::escape_IN_clause_values( $this->steps_post_types ) )
)
);
if ( ! empty( $course_steps_primary ) ) {
$course_steps_primary = array_map( 'intval', $course_steps_primary );
}
$course_steps_primary_intersect = array_intersect( $course_steps_new, $course_steps_primary );
$course_steps_primary_remove = array_diff( $course_steps_primary, $course_steps_primary_intersect );
if ( ! empty( $course_steps_primary_remove ) ) {
$wpdb->query(
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
$wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key = %s AND post_id IN (" . LDLMS_DB::escape_IN_clause_placeholders( $course_steps_primary_remove ) . ')', array_merge( array( 'course_id' ), LDLMS_DB::escape_IN_clause_values( $course_steps_primary_remove ) ) )
);
}
}
/**
* Set Steps to Course Legacy.
* This is used when the Course Option Share Steps is not used.
*
* @since 2.5.0
*/
protected function set_step_to_course_legacy() {
global $wpdb;
$course_steps_new = array();
if ( ( isset( $this->steps['t'] ) ) && ( ! empty( $this->steps['t'] ) ) ) {
foreach ( $this->steps['t'] as $step_type => $step_type_set ) {
if ( ! empty( $step_type_set ) ) {
$this->set_step_to_course_order( $step_type_set );
$course_steps_new = array_merge( $course_steps_new, $step_type_set );
}
}
}
// Finally we set the Course order to Menu Order/ASC so we can retain the ordering.
learndash_update_setting( $this->course_id, 'course_lesson_orderby', 'menu_order' );
learndash_update_setting( $this->course_id, 'course_lesson_order', 'ASC' );
if ( ! empty( $course_steps_new ) ) {
sort( $course_steps_new, SORT_NUMERIC );
}
$course_steps_old = $wpdb->get_col(
$wpdb->prepare( // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber
//phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
"SELECT posts.ID as post_id FROM {$wpdb->posts} as posts INNER JOIN {$wpdb->postmeta} as postmeta ON posts.ID = postmeta.post_id WHERE 1=1 AND postmeta.meta_key = %s AND postmeta.meta_value = %d AND posts.post_type IN (" . LDLMS_DB::escape_IN_clause_placeholders( $this->steps_post_types ) . ')',
array_merge( array( 'course_id' ), array( $this->course_id ), LDLMS_DB::escape_IN_clause_values( $this->steps_post_types ) )
)
);
if ( ! empty( $course_steps_old ) ) {
$course_steps_old = array_map( 'intval', $course_steps_old );
}
$course_steps_intersect = array_intersect( $course_steps_new, $course_steps_old );
// Add Steps.
$course_steps_add = array_diff( $course_steps_new, $course_steps_intersect );
if ( ! empty( $course_steps_add ) ) {
foreach ( $course_steps_add as $post_id ) {
learndash_update_setting( $post_id, 'course', $this->course_id );
}
}
// Remove Steps.
$course_steps_remove = array_diff( $course_steps_old, $course_steps_intersect );
if ( ! empty( $course_steps_remove ) ) {
foreach ( $course_steps_remove as $post_id ) {
learndash_update_setting( $post_id, 'course', 0 );
learndash_update_setting( $post_id, 'lesson', 0 );
}
}
if ( ( isset( $this->steps['h'] ) ) && ( ! empty( $this->steps['h'] ) ) ) {
$this->set_step_to_course_relationship( $this->steps['h'] );
}
}
/**
* Internal utility function to update the step postmeta with the parent course_id.
*
* @since 2.5.0
*
* @param array $steps Array of Course step nodes and items.
* @param int $parent_lesson_id Parent Lesson ID.
*/
private function set_step_to_course_relationship( $steps = array(), $parent_lesson_id = 0 ) {
global $wpdb;
if ( ! empty( $steps ) ) {
foreach ( $steps as $steps_type => $steps_type_set ) {
if ( learndash_get_post_type_slug( 'lesson' ) === $steps_type ) {
// A note about the queries. These should have been run through WP_Query
// but there is more overhead there than we need.
$sql_str = $wpdb->prepare(
'SELECT DISTINCT posts.ID
FROM ' . $wpdb->posts . ' as posts
INNER JOIN ' . $wpdb->postmeta . " as postmeta_course ON posts.ID=postmeta_course.post_id
WHERE 1=1
AND posts.post_type = %s
AND postmeta_course.meta_key = 'course_id' AND postmeta_course.meta_value = %d
",
$steps_type,
$this->course_id
);
} elseif ( ( learndash_get_post_type_slug( 'quiz' ) === $steps_type ) && ( 0 === $parent_lesson_id ) ) {
$sql_str = $wpdb->prepare(
'SELECT posts.ID FROM ' . $wpdb->posts . ' as posts
LEFT JOIN ' . $wpdb->postmeta . ' postmeta
ON ( posts.ID = postmeta.post_id )
LEFT JOIN ' . $wpdb->postmeta . ' AS mt1
ON ( posts.ID = mt1.post_id )
LEFT JOIN ' . $wpdb->postmeta . " AS mt2
ON (posts.ID = mt2.post_id AND mt2.meta_key = 'lesson_id' )
WHERE 1=1
AND (
( postmeta.meta_key = 'course_id'
AND CAST(postmeta.meta_value AS SIGNED) = %d )
AND
(
( mt1.meta_key = 'lesson_id' AND CAST(mt1.meta_value AS SIGNED) = '0' )
OR
mt2.post_id IS NULL
)
)
AND posts.post_type = %s
GROUP BY posts.ID ORDER BY posts.post_date DESC ",
$this->course_id,
$steps_type
);
} elseif ( ! empty( $parent_lesson_id ) ) {
$sql_str = $wpdb->prepare(
'SELECT DISTINCT posts.ID
FROM ' . $wpdb->posts . ' as posts
INNER JOIN ' . $wpdb->postmeta . ' as postmeta_course ON posts.ID=postmeta_course.post_id
INNER JOIN ' . $wpdb->postmeta . " as postmeta_lesson ON posts.ID=postmeta_lesson.post_id
WHERE 1=1
AND posts.post_type = %s
AND postmeta_course.meta_key = 'course_id' AND postmeta_course.meta_value = %d
AND postmeta_lesson.meta_key = 'lesson_id' AND postmeta_lesson.meta_value = %d
",
$steps_type,
$this->course_id,
$parent_lesson_id
);
}
if ( ! empty( $sql_str ) ) {
if ( ( is_array( $steps_type_set ) ) && ( count( $steps_type_set ) ) ) {
$step_type_ids_new = array_keys( $steps_type_set );
} else {
$step_type_ids_new = array();
}
$step_type_ids_old = $wpdb->get_col( $sql_str ); //phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- prepared SQL in prior lines
if ( ! empty( $step_type_ids_old ) ) {
$step_type_ids_old = array_map( 'intval', $step_type_ids_old );
}
$step_type_ids_intersect = array_intersect( $step_type_ids_new, $step_type_ids_old );
$step_type_ids_add = array_diff( $step_type_ids_new, $step_type_ids_intersect );
if ( ( ! empty( $step_type_ids_add ) ) && ( ! empty( $parent_lesson_id ) ) ) {
foreach ( $step_type_ids_add as $post_id ) {
learndash_update_setting( $post_id, 'lesson', $parent_lesson_id );
}
}
$step_type_ids_remove = array_diff( $step_type_ids_old, $step_type_ids_intersect );
if ( ! empty( $step_type_ids_remove ) ) {
foreach ( $step_type_ids_remove as $post_id ) {
learndash_update_setting( $post_id, 'lesson', 0 );
}
}
}
foreach ( $steps_type_set as $step_id => $step_id_set ) {
if ( ( is_array( $step_id_set ) ) && ( ! empty( $step_id_set ) ) ) {
$this->set_step_to_course_relationship( $step_id_set, $step_id );
}
}
}
}
}
/**
* Internal utility function to update the step postmeta menu_order value.
*
* @since 2.5.0
*
* @param array $steps Array of Course step nodes and items.
*/
private function set_step_to_course_order( $steps = array() ) {
global $wpdb;
if ( ! empty( $steps ) ) {
$sql_str = '';
foreach ( $steps as $step_order => $step_id ) {
++$step_order;
if ( ( defined( 'LEARNDASH_BUILDER_STEPS_UPDATE_POST' ) ) && ( true === LEARNDASH_BUILDER_STEPS_UPDATE_POST ) ) {
$edit_post = array(
'ID' => $step_id,
'menu_order' => $step_order,
);
wp_update_post( $edit_post );
} else {
$update_ret = $wpdb->update(
$wpdb->posts,
array( 'menu_order' => $step_order ),
array( 'ID' => $step_id ),
array( '%d' ),
array( '%d' )
);
if ( ( $update_ret ) && ( ! is_wp_error( $update_ret ) ) ) {
clean_post_cache( $step_id );
}
}
}
}
}
/**
* Load Course Steps for legacy setup (non-shared steps).
*
* @since 2.5.0
*/
private function load_steps_legacy() {
$steps = array();
$this->objects_loaded = false;
$this->objects = array();
if ( ! empty( $this->course_id ) ) {
// Set that we loaded the objects to prevent double logic.
$this->objects_loaded = true;
/**
* If Course Builder is enabled and the meta is not empty we set the
* orderby/order instead of using the global settings.
* See LEARNDASH-5804
*/
if ( ( true === $this->meta['course_builder_enabled'] ) && ( true !== $this->meta['empty'] ) ) {
$course_lesson_order = array(
'order' => 'ASC',
'orderby' => 'menu_order',
);
} else {
$course_lesson_order = learndash_get_course_lessons_order( $this->course_id );
}
// Course > Lessons.
$lesson_steps_query_args = array(
'post_type' => learndash_get_post_type_slug( 'lesson' ),
'posts_per_page' => -1,
'post_status' => $this->get_step_post_statuses(),
'orderby' => $course_lesson_order['orderby'],
'order' => $course_lesson_order['order'],
'meta_query' => array(
array(
'key' => 'course_id',
'value' => absint( $this->course_id ),
'compare' => '=',
),
),
);
$lesson_steps_query = new WP_Query( $lesson_steps_query_args );
if ( ( is_a( $lesson_steps_query, 'WP_Query' ) ) && ( property_exists( $lesson_steps_query, 'posts' ) ) && ( ! empty( $lesson_steps_query->posts ) ) ) {
foreach ( $lesson_steps_query->posts as $lesson ) {
$steps[ learndash_get_post_type_slug( 'lesson' ) ][ $lesson->ID ] = array();
$steps[ learndash_get_post_type_slug( 'lesson' ) ][ $lesson->ID ][ learndash_get_post_type_slug( 'topic' ) ] = array();
$steps[ learndash_get_post_type_slug( 'lesson' ) ][ $lesson->ID ][ learndash_get_post_type_slug( 'quiz' ) ] = array();
$this->objects[ $lesson->ID ] = $lesson;
// Course > Lesson > Topics.
$topic_steps_query_args = array(
'post_type' => learndash_get_post_type_slug( 'topic' ),
'posts_per_page' => -1,
'post_status' => $this->get_step_post_statuses(),
'orderby' => $course_lesson_order['orderby'],
'order' => $course_lesson_order['order'],
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'course_id',
'value' => absint( $this->course_id ),
'compare' => '=',
),
array(
'key' => 'lesson_id',
'value' => absint( $lesson->ID ),
'compare' => '=',
),
),
);
$topic_steps_query = new WP_Query( $topic_steps_query_args );
if ( ( is_a( $topic_steps_query, 'WP_Query' ) ) && ( property_exists( $topic_steps_query, 'posts' ) ) && ( ! empty( $topic_steps_query->posts ) ) ) {
foreach ( $topic_steps_query->posts as $topic ) {
$steps[ learndash_get_post_type_slug( 'lesson' ) ][ $lesson->ID ][ learndash_get_post_type_slug( 'topic' ) ][ $topic->ID ] = array();
$steps[ learndash_get_post_type_slug( 'lesson' ) ][ $lesson->ID ][ learndash_get_post_type_slug( 'topic' ) ][ $topic->ID ][ learndash_get_post_type_slug( 'quiz' ) ] = array();
$this->objects[ $topic->ID ] = $topic;
// Course > Lesson > Topic > Quizzes.
$topic_quiz_steps_query_args = array(
'post_type' => learndash_get_post_type_slug( 'quiz' ),
'posts_per_page' => -1,
'post_status' => $this->get_step_post_statuses(),
'orderby' => $course_lesson_order['orderby'],
'order' => $course_lesson_order['order'],
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'course_id',
'value' => absint( $this->course_id ),
'compare' => '=',
),
array(
'key' => 'lesson_id',
'value' => absint( $topic->ID ),
'compare' => '=',
),
),
);
$topic_quiz_steps_query = new WP_Query( $topic_quiz_steps_query_args );
if ( ( is_a( $topic_quiz_steps_query, 'WP_Query' ) ) && ( property_exists( $topic_quiz_steps_query, 'posts' ) ) && ( ! empty( $topic_quiz_steps_query->posts ) ) ) {
foreach ( $topic_quiz_steps_query->posts as $quiz ) {
$steps[ learndash_get_post_type_slug( 'lesson' ) ][ $lesson->ID ][ learndash_get_post_type_slug( 'topic' ) ][ $topic->ID ][ learndash_get_post_type_slug( 'quiz' ) ][ $quiz->ID ] = array();
$this->objects[ $quiz->ID ] = $quiz;
}
}
}
}
// Course > Lesson > Quizzes.
$lesson_quiz_steps_query_args = array(
'post_type' => learndash_get_post_type_slug( 'quiz' ),
'posts_per_page' => -1,
'post_status' => $this->get_step_post_statuses(),
'orderby' => $course_lesson_order['orderby'],
'order' => $course_lesson_order['order'],
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'course_id',
'value' => absint( $this->course_id ),
'compare' => '=',
),
array(
'key' => 'lesson_id',
'value' => absint( $lesson->ID ),
'compare' => '=',
),
),
);
$lesson_quiz_steps_query = new WP_Query( $lesson_quiz_steps_query_args );
if ( ( is_a( $lesson_quiz_steps_query, 'WP_Query' ) ) && ( property_exists( $lesson_quiz_steps_query, 'posts' ) ) && ( ! empty( $lesson_quiz_steps_query->posts ) ) ) {
foreach ( $lesson_quiz_steps_query->posts as $quiz ) {
$steps[ learndash_get_post_type_slug( 'lesson' ) ][ $lesson->ID ][ learndash_get_post_type_slug( 'quiz' ) ][ $quiz->ID ] = array();
$this->objects[ $quiz->ID ] = $quiz;
}
}
}
} else {
$steps[ learndash_get_post_type_slug( 'lesson' ) ] = array();
}
// Course > Quizzes (Global Quizzes).
$quiz_steps_query_args = array(
'post_type' => learndash_get_post_type_slug( 'quiz' ),
'posts_per_page' => -1,
'post_status' => $this->get_step_post_statuses(),
'orderby' => $course_lesson_order['orderby'],
'order' => $course_lesson_order['order'],
'meta_query' => array(
'relation' => 'AND',
array(
'key' => 'course_id',
'value' => absint( $this->course_id ),
'compare' => '=',
),
),
);
$quiz_ids = array();
if ( ! empty( $this->objects ) ) {
foreach ( $this->objects as $step_object ) {
if ( learndash_get_post_type_slug( 'quiz' ) === $step_object->post_type ) {
$quiz_ids[] = $step_object->ID;
}
}
}
if ( ! empty( $quiz_ids ) ) {
$quiz_steps_query_args['post__not_in'] = $quiz_ids;
}
$quiz_steps_query = new WP_Query( $quiz_steps_query_args );
if ( ( is_a( $quiz_steps_query, 'WP_Query' ) ) && ( property_exists( $quiz_steps_query, 'posts' ) ) && ( ! empty( $quiz_steps_query->posts ) ) ) {
foreach ( $quiz_steps_query->posts as $quiz ) {
$steps[ learndash_get_post_type_slug( 'quiz' ) ][ $quiz->ID ] = array();
$this->objects[ $quiz->ID ] = $quiz;
}
} else {
$steps[ learndash_get_post_type_slug( 'quiz' ) ] = array();
}
}
return $steps;
}
/**
* Returns the parent steps for a given step. Empty array if no parent steps.
*
* @since 2.5.0
*
* @param int $post_id Current step post ID.
* @param string $post_type Parent step post_type to.
*/
public function get_item_parent_steps( $post_id = 0, $post_type = '' ) {
$item_ancestor_steps = array();
if ( ! empty( $post_id ) ) {
if ( empty( $post_type ) ) {
$post_type = get_post_type( $post_id );
}
if ( ! empty( $post_type ) ) {
$this->load_steps();
$steps_key = $post_type . ':' . $post_id;
if ( isset( $this->steps['r'][ $steps_key ] ) ) {
$item_ancestor_steps = $this->steps['r'][ $steps_key ];
}
}
}
return $item_ancestor_steps;
}
/**
* Returns the parent step ID for a given step. 0 if no parent step.
*
* @since 2.5.0
*
* @param int $step_post_id Current step post ID.
* @param string $ancestor_step_type Parent step post_type to return.
*/
public function get_parent_step_id( $step_post_id = 0, $ancestor_step_type = '' ) {
if ( ! empty( $step_post_id ) ) {
$step_ancestor_item = $this->get_item_parent_steps( $step_post_id );
if ( ! empty( $step_ancestor_item ) ) {
foreach ( $step_ancestor_item as $parent_steps_value ) {
if ( ( is_string( $parent_steps_value ) ) && ( ! empty( $parent_steps_value ) ) ) {
list( $s_post_type, $s_post_id ) = explode( ':', $parent_steps_value );
if ( ! empty( $ancestor_step_type ) ) {
if ( $ancestor_step_type == $s_post_type ) {
return intval( $s_post_id );
}
} else {
return intval( $s_post_id );
}
}
}
}
}
}
/**
* Get Parent Children steps.
*
* @since 2.6.0
* @since 3.4.0 Added $nested parameter.
*
* @param int $parent_post_id The parent post ID.
* @param string $return_post_type Return specific post type.
* @param string $return_type Return type. Default 'ids'. Other values 'objects'.
* @param bool $nested Wether to traverse substeps. Default false.
*/
public function get_children_steps( $parent_post_id = 0, $return_post_type = '', $return_type = 'ids', $nested = false ) {
$item_children_steps = array();
if ( ! empty( $parent_post_id ) ) {
$this->load_steps();
$steps_h = $this->get_steps( 'h' );
/**
* Here we need to drill through the top-level step (lessons) to get ot our parent_post_type
*/
$ancestor_steps = $this->get_item_parent_steps( $parent_post_id, get_post_type( $parent_post_id ) );
if ( ! empty( $ancestor_steps ) ) {
$ancestor_steps = array_reverse( $ancestor_steps );
}
$ancestor_steps[] = get_post_type( $parent_post_id ) . ':' . $parent_post_id;
foreach ( $ancestor_steps as $ancestor_step ) {
if ( ( is_string( $ancestor_step ) ) && ( ! empty( $ancestor_step ) ) ) {
list( $ancestor_step_post_type, $ancestor_step_post_id ) = explode( ':', $ancestor_step );
if ( isset( $steps_h[ $ancestor_step_post_type ][ $ancestor_step_post_id ] ) ) {
$steps_h = $steps_h[ $ancestor_step_post_type ][ $ancestor_step_post_id ];
}
}
}
if ( ! empty( $steps_h ) ) {
foreach ( $steps_h as $steps_post_type => $steps_post_set ) {
if ( ( empty( $return_post_type ) ) || ( $return_post_type == $steps_post_type ) && ( is_array( $steps_post_set ) ) ) {
$item_children_steps = array_merge( $item_children_steps, array_keys( $steps_post_set ) );
}
if ( ( true === $nested ) && ( ! empty( $steps_post_set ) ) ) {
foreach ( $steps_post_set as $sub_step_id => $sub_steps_post_set ) {
$sub_item_children_steps = $this->get_children_steps( $sub_step_id, $return_post_type, $return_type, $nested );
if ( ! empty( $sub_item_children_steps ) ) {
$item_children_steps = array_merge( $item_children_steps, $sub_item_children_steps );
}
}
}
}
}
}
if ( 'ids' !== $return_type ) {
$item_children_objects = array();
foreach ( $item_children_steps as $step_id ) {
if ( isset( $this->objects[ $step_id ] ) ) {
$item_children_objects[ $step_id ] = $this->objects[ $step_id ];
}
}
$item_children_steps = $item_children_objects;
}
return $item_children_steps;
}
/**
* Steps split keys.
*
* @since 2.5.0
*
* @param array $steps Array of steps.
* @param string $parent_type Parent Post Type slug.
*/
public static function steps_split_keys( $steps, $parent_type = '' ) {
if ( learndash_get_post_type_slug( 'lesson' ) === $parent_type ) {
$course_steps_split_keys = array(
'sfwd-topic' => array(),
'sfwd-quiz' => array(),
);
} elseif ( learndash_get_post_type_slug( 'topic' ) === $parent_type ) {
$course_steps_split_keys = array(
'sfwd-quiz' => array(),
);
} elseif ( learndash_get_post_type_slug( 'quiz' ) === $parent_type ) {
$course_steps_split_keys = array();
} elseif ( 'section-heading' === $parent_type ) {
$course_steps_split_keys = array();
} elseif ( empty( $parent_type ) ) {
$course_steps_split_keys = array(
'sfwd-lessons' => array(),
'sfwd-quiz' => array(),
);
}
if ( ! empty( $steps ) ) {
foreach ( $steps as $step_idx => $step_set ) {
list( $step_post_type, $step_id ) = explode( ':', $step_idx );
if ( ( ! empty( $step_post_type ) ) && ( ! empty( $step_id ) ) ) {
if ( ! isset( $course_steps_split_keys[ $step_post_type ] ) ) {
$course_steps_split_keys[ $step_post_type ] = array();
}
if ( $step_post_type === 'section-heading' ) {
$course_steps_split_keys[ $step_post_type ][ $step_id ] = $step_set;
} else {
$course_steps_split_keys[ $step_post_type ][ $step_id ] = self::steps_split_keys( $step_set, $step_post_type );
}
}
}
}
return $course_steps_split_keys;
}
/**
* Get the array of 'post_stats' keys used for Course Steps queries.
*
* @since 3.4.0
*
* @return array Array of post_status keys.
*/
protected function get_step_post_statuses() {
$post_status_keys = array();
$post_statuses = get_post_stati(
array(
'internal' => false,
'_builtin' => true,
)
);
if ( ! empty( $post_statuses ) ) {
$post_status_keys = array_keys( $post_statuses );
}
/**
* Filters the $post_status_keys use for Course Steps Queries.
*
* @since 3.4.0
*
* @param array $post_status_keys Array of post_status keys.
*/
return apply_filters( 'learndash_course_steps_post_status_keys', $post_status_keys );
}
}
}