class-ld-rest-users-course-progress-controller.php 13 KB
<?php
/**
 * LearnDash REST API V1 Course Progress Controller.
 *
 * @since 2.5.8
 * @package LearnDash\REST\V1
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( ( ! class_exists( 'LD_REST_Users_Course_Progress_Controller_V1' ) ) && ( class_exists( 'LD_REST_Posts_Controller_V1' ) ) ) {

	/**
	 * Class LearnDash REST API V1 Course Progress Controller.
	 *
	 * @since 2.5.8
	 */
	class LD_REST_Users_Course_Progress_Controller_V1 extends LD_REST_Posts_Controller_V1 /* phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound */ {

		/**
		 * Public constructor for class
		 *
		 * @since 2.5.8
		 */
		public function __construct() {
			$this->post_type  = 'sfwd-courses';
			$this->taxonomies = array();

			parent::__construct( $this->post_type );
			$this->namespace = LEARNDASH_REST_API_NAMESPACE . '/' . $this->version;
			$this->rest_base = LearnDash_Settings_Section::get_section_setting( 'LearnDash_Settings_Section_General_REST_API', 'users' );
		}

		/**
		 * Registers the routes for the objects of the controller.
		 *
		 * @since 2.5.8
		 *
		 * @see register_rest_route() in WordPress core.
		 */
		public function register_routes() {

			$collection_params = $this->get_collection_params();
			$schema            = $this->get_item_schema();

			$get_item_args = array(
				'context' => $this->get_context_param( array( 'default' => 'view' ) ),
			);

			register_rest_route(
				$this->namespace,
				'/' . $this->rest_base . '/(?P<id>[\d]+)/course-progress/',
				array(
					'args' => array(
						'id' => array(
							// translators: course.
							'description' => sprintf( esc_html_x( 'User ID to show %s progress', 'placeholder: course', 'learndash' ), learndash_get_custom_label_lower( 'course' ) ),
							'required'    => true,
							'type'        => 'integer',
						),
					),
					array(
						'methods'             => 'GET',
						'callback'            => array( $this, 'get_users_progress' ),
						'permission_callback' => array( $this, 'get_users_progress_permissions_check' ),
						'args'                => $this->get_collection_params(),
					),
				)
			);
		}

		/**
		 * Check User Progress Read Permissions.
		 *
		 * @since 2.5.8
		 *
		 * @param object $request WP_REST_Request instance.
		 */
		public function get_users_progress_permissions_check( $request ) {
			$user_id = $request['id'];

			if ( learndash_is_admin_user() ) {
				return true;
			} elseif ( get_current_user_id() === $user_id ) {
				return true;
			} elseif ( learndash_is_group_leader_user() ) {
				if ( learndash_is_group_leader_of_user( get_current_user_id(), $user_id ) ) {
					return true;
				}
			}
		}

		/**
		 * Get User Progress.
		 *
		 * @since 2.5.8
		 *
		 * @param object $request WP_REST_Request instance.
		 */
		public function get_users_progress( $request ) {
			$user_id = $request['id'];
			if ( empty( $user_id ) ) {
				return new WP_Error( 'rest_user_invalid_id', esc_html__( 'Invalid user ID. #1', 'learndash' ), array( 'status' => 404 ) );
			}

			if ( is_user_logged_in() ) {
				$current_user_id = get_current_user_id();
			} else {
				$current_user_id = 0;
			}

			$data = array();

			$user_course_progress = get_user_meta( $user_id, '_sfwd-course_progress', true );
			$user_course_progress = ! empty( $user_course_progress ) ? $user_course_progress : array();

			$courses_registered = ld_get_mycourses( $user_id );
			$courses_registered = ! empty( $courses_registered ) ? $courses_registered : array();

			$user_course_ids = array_keys( $user_course_progress );
			$user_course_ids = array_merge( $user_course_ids, $courses_registered );
			$user_course_ids = array_unique( $user_course_ids );

			if ( ( ! empty( $user_course_ids ) ) && ( learndash_is_group_leader_user() ) ) {
				$gl_groups_corses = learndash_get_group_leader_groups_courses( get_current_user_id() );

				if ( ! empty( $gl_groups_corses ) ) {
					$user_course_ids = array_intersect( $gl_groups_corses, $user_course_ids );
				}
			}

			if ( ! empty( $user_course_ids ) ) {

				// Ensure a search string is set in case the orderby is set to 'relevance'.
				if ( ! empty( $request['orderby'] ) && 'relevance' === $request['orderby'] && empty( $request['search'] ) ) {
					return new WP_Error( 'rest_no_search_term_defined', __( 'You need to define a search term to order by relevance.', 'learndash' ), array( 'status' => 400 ) );
				}

				// Ensure an include parameter is set in case the orderby is set to 'include'.
				if ( ! empty( $request['orderby'] ) && 'include' === $request['orderby'] && empty( $request['include'] ) ) {
					return new WP_Error( 'rest_orderby_include_missing_include', __( 'You need to define an include parameter to order by include.', 'learndash' ), array( 'status' => 400 ) );
				}

				// Retrieve the list of registered collection query parameters.
				$registered = $this->get_collection_params();
				$args       = array();

				/*
				 * This array defines mappings between public API query parameters whose
				 * values are accepted as-passed, and their internal WP_Query parameter
				 * name equivalents (some are the same). Only values which are also
				 * present in $registered will be set.
				 */
				$parameter_mappings = array(
					'author'         => 'author__in',
					'author_exclude' => 'author__not_in',
					'exclude'        => 'post__not_in',
					'include'        => 'post__in',
					'menu_order'     => 'menu_order',
					'offset'         => 'offset',
					'order'          => 'order',
					'orderby'        => 'orderby',
					'page'           => 'paged',
					'parent'         => 'post_parent__in',
					'parent_exclude' => 'post_parent__not_in',
					'search'         => 's',
					'slug'           => 'post_name__in',
					'status'         => 'post_status',
				);

				/*
				 * For each known parameter which is both registered and present in the request,
				 * set the parameter's value on the query $args.
				 */
				foreach ( $parameter_mappings as $api_param => $wp_param ) {
					if ( isset( $registered[ $api_param ], $request[ $api_param ] ) ) {
						$args[ $wp_param ] = $request[ $api_param ];
					}
				}

				// Check for & assign any parameters which require special handling or setting.
				$args['date_query'] = array();

				// Set before into date query. Date query must be specified as an array of an array.
				if ( isset( $registered['before'], $request['before'] ) ) {
					$args['date_query'][0]['before'] = $request['before'];
				}

				// Set after into date query. Date query must be specified as an array of an array.
				if ( isset( $registered['after'], $request['after'] ) ) {
					$args['date_query'][0]['after'] = $request['after'];
				}

				// Ensure our per_page parameter overrides any provided posts_per_page filter.
				if ( isset( $registered['per_page'] ) ) {
					$args['posts_per_page'] = $request['per_page'];
				}

				if ( isset( $registered['sticky'], $request['sticky'] ) ) {
					$sticky_posts = get_option( 'sticky_posts', array() );
					if ( ! is_array( $sticky_posts ) ) {
						$sticky_posts = array();
					}
					if ( $request['sticky'] ) {
						/*
						 * As post__in will be used to only get sticky posts,
						 * we have to support the case where post__in was already
						 * specified.
						 */
						$args['post__in'] = $args['post__in'] ? array_intersect( $sticky_posts, $args['post__in'] ) : $sticky_posts;

						/*
						 * If we intersected, but there are no post ids in common,
						 * WP_Query won't return "no posts" for post__in = array()
						 * so we have to fake it a bit.
						 */
						if ( ! $args['post__in'] ) {
							$args['post__in'] = array( 0 );
						}
					} elseif ( $sticky_posts ) {
						/*
						 * As post___not_in will be used to only get posts that
						 * are not sticky, we have to support the case where post__not_in
						 * was already specified.
						 */
						$args['post__not_in'] = array_merge( $args['post__not_in'], $sticky_posts );
					}
				}

				// Force the post_type argument, since it's not a user input variable.
				$args['post_type'] = $this->post_type;
				$args['post__in']  = $user_course_ids;
				$args['fields']    = 'ids';

				/**
				 * Filters the query arguments for user course progress REST request.
				 *
				 * Enables adding extra arguments or setting defaults for a post collection request.
				 *
				 * @since 2.5.8
				 *
				 * @link https://developer.wordpress.org/reference/classes/wp_query/
				 *
				 * @param array           $args    An array of query arguments for getting users course progress.
				 * @param WP_REST_Request $request The REST request object.
				 */
				$args       = apply_filters( 'learndash_rest_users_course_progress_query', $args, $request );
				$query_args = $this->prepare_items_query( $args, $request );

				$taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_rest' => true ) );

				foreach ( $taxonomies as $taxonomy ) {
					$base        = ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;
					$tax_exclude = $base . '_exclude';

					if ( ! empty( $request[ $base ] ) ) {
						$query_args['tax_query'][] = array(
							'taxonomy'         => $taxonomy->name,
							'field'            => 'term_id',
							'terms'            => $request[ $base ],
							'include_children' => false,
						);
					}

					if ( ! empty( $request[ $tax_exclude ] ) ) {
						$query_args['tax_query'][] = array(
							'taxonomy'         => $taxonomy->name,
							'field'            => 'term_id',
							'terms'            => $request[ $tax_exclude ],
							'include_children' => false,
							'operator'         => 'NOT IN',
						);
					}
				}

				$posts_query  = new WP_Query();
				$query_result = $posts_query->query( $query_args );

				// Allow access to all password protected posts if the context is edit.
				if ( 'edit' === $request['context'] ) {
					add_filter( 'post_password_required', '__return_false' );
				}

				foreach ( $query_result as $course_id ) {
					$data[ $course_id ] = array();

					if ( isset( $user_course_progress[ $course_id ] ) ) {
						$converted = $this->user_meta_progress_normalized( $user_course_progress[ $course_id ] );
					} else {
						$converted = array();
					}

					$ld_course_steps_object = LDLMS_Factory_Post::course_steps( intval( $course_id ) );
					$ld_course_steps_object->load_steps();
					$course_steps_l = $ld_course_steps_object->get_steps( 'l' );
					if ( ! empty( $course_steps_l ) ) {
						foreach ( $course_steps_l as $step_key ) {
							list( $step_type, $step_id ) = explode( ':', $step_key );
							if ( isset( $converted[ $step_key ] ) ) {
								$completed = $converted[ $step_key ];
							} else {
								$completed = 0;
							}
							$data[ $course_id ][ $step_id ] = $completed;
						}
					}
				}

				// Reset filter.
				if ( 'edit' === $request['context'] ) {
					remove_filter( 'post_password_required', '__return_false' );
				}

				$page        = (int) $query_args['paged'];
				$total_posts = $posts_query->found_posts;

				if ( $total_posts < 1 ) {
					// Out-of-bounds, run the query again without LIMIT for total count.
					unset( $query_args['paged'] );

					$count_query = new WP_Query();
					$count_query->query( $query_args );
					$total_posts = $count_query->found_posts;
				}

				$max_pages = ceil( $total_posts / (int) $posts_query->query_vars['posts_per_page'] );

				if ( $page > $max_pages && $total_posts > 0 ) {
					return new WP_Error( 'rest_post_invalid_page_number', __( 'The page number requested is larger than the number of pages available.', 'learndash' ), array( 'status' => 400 ) );
				}

				$response = rest_ensure_response( $data );

				$response->header( 'X-WP-Total', (int) $total_posts );
				$response->header( 'X-WP-TotalPages', (int) $max_pages );

				$request_params = $request->get_query_params();
				$base           = add_query_arg( $request_params, rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ) );

				if ( $page > 1 ) {
					$prev_page = $page - 1;

					if ( $prev_page > $max_pages ) {
						$prev_page = $max_pages;
					}

					$prev_link = add_query_arg( 'page', $prev_page, $base );
					$response->link_header( 'prev', $prev_link );
				}
				if ( $max_pages > $page ) {
					$next_page = $page + 1;
					$next_link = add_query_arg( 'page', $next_page, $base );

					$response->link_header( 'next', $next_link );
				}
			}
			return $response;
		}

		/**
		 * Get User Meta Progression.
		 *
		 * @since 2.5.8
		 *
		 * @param array $progress Array of course progress data.
		 */
		public function user_meta_progress_normalized( $progress = array() ) {
			$converted = array();

			if ( ( isset( $progress['lessons'] ) ) && ( ! empty( $progress['lessons'] ) ) ) {
				foreach ( $progress['lessons']  as $lesson_id => $lesson_complete ) {
					$converted[ 'sfwd-lessons:' . $lesson_id ] = $lesson_complete;
					if ( ( isset( $progress['topics'][ $lesson_id ] ) ) && ( ! empty( $progress['topics'][ $lesson_id ] ) ) ) {
						foreach ( $progress['topics'][ $lesson_id ]  as $topic_id => $topic_complete ) {
							$converted[ 'sfwd-topic:' . $topic_id ] = $topic_complete;
						}
					}
				}
			}

			return $converted;
		}
	}
}