gocodebox/lifterlms-rest

View on GitHub
includes/server/class-llms-rest-lessons-controller.php

Summary

Maintainability
F
3 days
Test Coverage
A
95%
<?php
/**
 * REST Lessons Controller
 *
 * @package LifterLMS_REST/Classes/Controllers
 *
 * @since 1.0.0-beta.1
 * @version 1.0.0-beta.27
 */

defined( 'ABSPATH' ) || exit;


/**
 * LLMS_REST_Lessons_Controller class.
 *
 * @since 1.0.0-beta.1
 * @since 1.0.0-beta.7 `prepare_objects_query()` renamed to `prepare_collection_query_args()`.
 *                     Added the following properties to the item schema: `drip_date`, `drip_days`, `drip_method`, `public`, `quiz`.
 *                     Added the following links: `prerequisite`, `quiz`.
 *                     Fixed `siblings` link that was using the parent course's id instead of the parent section's id.
 *                     Fixed `parent` link href, replacing 'section' with 'sections'.
 *                     Added following properties to the response object: `public`, `points`, `quiz`, `drip_method`, `drip_days`, `drip_date`, `prerequisite`.
 *                     Fixed lesson progression callback name when defining the filters to be removed while preparing the item for response.
 *                     Added `llms_rest_lesson_item_schema`, `llms_rest_pre_insert_lesson`, `llms_rest_prepare_lesson_object_response`, `llms_rest_lesson_links` filter hooks.
 *                     Added `prepare_item_for_database()`, `update_additional_object_fields()` method.
 * @since 1.0.0-beta.8 Call `set_bulk()` llms post method passing `true` as second parameter,
 *                     so to instruct it to return a WP_Error on failure.
 * @since 1.0.0-beta.9 Removed `create_llms_post()` and `get_object()` methods, now abstracted in `LLMS_REST_Posts_Controller` class.
 *                     `llms_rest_lesson_filters_removed_for_response` filter hook added.
 * @since 1.0.0-beta.12 Updated `$this->prepare_collection_query_args()` to reflect changes in the parent class.
 * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`.
 */
class LLMS_REST_Lessons_Controller extends LLMS_REST_Posts_Controller {

    /**
     * Route base.
     *
     * @var string
     */
    protected $rest_base = 'lessons';

    /**
     * Post type.
     *
     * @var string
     */
    protected $post_type = 'lesson';

    /**
     * Schema properties available for ordering the collection.
     *
     * @var string[]
     */
    protected $orderby_properties = array(
        'id',
        'title',
        'date_created',
        'date_updated',
        'order',
        'relevance',
    );

    /**
     * Parent section id.
     *
     * @var int
     */
    protected $parent_id;

    /**
     * Meta field names to skip (added via `register_meta()`).
     *
     * @var string[]
     */
    protected $disallowed_meta_fields = array(
        '_llms_assignment',
        '_llms_quiz',
    );

    /**
     * Constructor.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.27 Call parent constructor.
     *
     * @return void
     */
    public function __construct() {

        parent::__construct();

        $this->collection_params = $this->build_collection_params();
    }

    /**
     * Set parent id.
     *
     * @since 1.0.0-beta.1
     *
     * @param int $parent_id Course parent id.
     * @return void
     */
    public function set_parent_id( $parent_id ) {
        $this->parent_id = $parent_id;
    }

    /**
     * Get parent id.
     *
     * @since 1.0.0-beta.1
     *
     * @return int|null Course parent id. Null if not set.
     */
    public function get_parent_id() {
        return isset( $this->parent_id ) ? $this->parent_id : null;
    }

    /**
     * Prepares a single lesson for create or update.
     *
     * @since 1.0.0-beta.7
     * @since 1.0.0-beta.15 Fixed setting/updating parent section/course.
     * @since 1.0.0-beta.23 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.
     * @since 1.0.0-beta.25 Remove now useless check on the existing parent course id when setting it automatically on parent section's id update.
     *
     * @param WP_REST_Request $request Request object.
     * @return array|WP_Error Array of lesson args or WP_Error.
     */
    protected function prepare_item_for_database( $request ) {

        $prepared_item = parent::prepare_item_for_database( $request );
        $schema        = $this->get_item_schema();

        // Lesson's audio embed URL.
        if ( ! empty( $schema['properties']['audio_embed'] ) && isset( $request['audio_embed'] ) ) {
            $prepared_item['audio_embed'] = $request['audio_embed'];
        }

        // Lesson's video embed URL.
        if ( ! empty( $schema['properties']['video_embed'] ) && isset( $request['video_embed'] ) ) {
            $prepared_item['video_embed'] = $request['video_embed'];
        }

        // Parent (section) id.
        if ( ! empty( $schema['properties']['parent_id'] ) && isset( $request['parent_id'] ) ) {

            // Retrieve the parent section.
            $parent_section = llms_get_post( $request['parent_id'] );

            $prepared_item['parent_section'] = $parent_section && is_a( $parent_section, 'LLMS_Section' ) ? $request['parent_id'] : 0;

            // Retrieve the parent course id.
            if ( $prepared_item['parent_section'] ) {
                $parent_course = $parent_section->get_course();
            }

            $prepared_item['parent_course'] = ! empty( $parent_course ) && is_a( $parent_course, 'LLMS_Course' ) ? $parent_course->get( 'id' ) : 0;

        }

        // Course id.
        if ( ! empty( $schema['properties']['course_id'] ) && isset( $request['course_id'] ) ) {

            $parent_course = llms_get_post( $request['course_id'] );

            if ( ! $parent_course || ! is_a( $parent_course, 'LLMS_Course' ) ) {
                return llms_rest_bad_request_error( __( 'Invalid course_id param. It must be a valid Course ID.', 'lifterlms' ) );
            }

            $prepared_item['parent_course'] = $request['course_id'];
        }

        // Order.
        if ( ! empty( $schema['properties']['order'] ) && isset( $request['order'] ) ) {

            // order must be > 0. It's sanitized as absint so it cannot come as negative value.
            if ( 0 === $request['order'] ) {
                return llms_rest_bad_request_error( __( 'Invalid order param. It must be greater than 0.', 'lifterlms' ) );
            }

            $prepared_item['order'] = $request['order'];
        }

        // Public (free lesson).
        if ( ! empty( $schema['properties']['public'] ) && isset( $request['public'] ) ) {
            $prepared_item['free_lesson'] = empty( $request['public'] ) ? 'no' : 'yes';
        }

        // Points.
        if ( ! empty( $schema['properties']['points'] ) && isset( $request['points'] ) ) {
            $prepared_item['points'] = $request['points'];
        }

        // Drip days.
        if ( ! empty( $schema['properties']['drip_days'] ) && isset( $request['drip_days'] ) ) {

            // drip_days must be > 0. It's sanitized as absint so it cannot come as negative value.
            if ( 0 === $request['drip_days'] ) {
                return llms_rest_bad_request_error( __( 'Invalid drip_days param. It must be greater than 0.', 'lifterlms' ) );
            }

            $prepared_item['days_before_available'] = $request['drip_days'];
        }

        // Drip date.
        if ( ! empty( $schema['properties']['drip_date'] ) && isset( $request['drip_date'] ) ) {
            $drip_date = rest_parse_date( $request['drip_date'] );

            // Drip date is nullable.
            if ( empty( $drip_date ) ) {
                $prepared_item['date_available'] = '';
                $prepared_item['time_available'] = '';
            } else {
                $prepared_item['date_available'] = date_i18n( 'Y-m-d', $drip_date );
                $prepared_item['time_available'] = date_i18n( 'H:i:s', $drip_date );
            }
        }

        // Drip method.
        if ( ! empty( $schema['properties']['drip_method'] ) && isset( $request['drip_method'] ) ) {
            $prepared_item['drip_method'] = 'none' === $request['drip_method'] ? '' : $request['drip_method'];
        }

        // Quiz enabled.
        if ( ! empty( $schema['properties']['quiz']['properties']['enabled'] ) && isset( $request['quiz']['enabled'] ) ) {
            $prepared_item['quiz_enabled'] = empty( $request['quiz']['enabled'] ) ? 'no' : 'yes';
        }

        // Quiz id.
        if ( ! empty( $schema['properties']['quiz']['properties']['id'] ) && isset( $request['quiz']['id'] ) ) {

            // check if quiz exists.
            $quiz = llms_get_post( $request['quiz']['id'] );

            if ( is_a( $quiz, 'LLMS_Quiz' ) ) {
                $prepared_item['quiz'] = $request['quiz']['id'];
            }
        }

        // Quiz progression.
        if ( ! empty( $schema['properties']['quiz']['properties']['progression'] ) && isset( $request['quiz']['progression'] ) ) {
            $prepared_item['require_passing_grade'] = 'complete' === $request['quiz']['progression'] ? 'no' : 'yes';
        }

        /**
         * Filters a lesson before it is inserted via the REST API.
         *
         * @since 1.0.0-beta.7
         *
         * @param array           $prepared_item Array of lesson item properties prepared for database.
         * @param WP_REST_Request $request       Full details about the request.
         * @param array           $schema        The item schema.
         */
        return apply_filters( 'llms_rest_pre_insert_lesson', $prepared_item, $request, $schema );
    }

    /**
     * Updates a single llms lesson.
     *
     * @since 1.0.0-beta.7
     * @since 1.0.0-beta.8 Call `set_bulk()` llms post method passing `true` as second parameter,
     *                     so to instruct it to return a WP_Error on failure.
     * @since 1.0.0-beta.25 Allow updating meta with the same value as the stored one.
     *
     * @param LLMS_Lesson     $lesson        LLMS_Lesson instance.
     * @param WP_REST_Request $request       Full details about the request.
     * @param array           $schema        The item schema.
     * @param array           $prepared_item Prepared item array.
     * @param bool            $creating      Optional. Whether we're in creation or update phase. Default true (create).
     * @return bool|WP_Error True on success or false if nothing to update, WP_Error object if something went wrong during the update.
     */
    protected function update_additional_object_fields( $lesson, $request, $schema, $prepared_item, $creating = true ) {

        $error = new WP_Error();

        $to_set = array();

        // Prerequisite.
        if ( ! empty( $schema['properties']['prerequisite'] ) && isset( $request['prerequisite'] ) ) {

            // check if lesson exists.
            $prerequisite = llms_get_post( $request['prerequisite'] );

            if ( is_a( $prerequisite, 'LLMS_Lesson' ) ) {
                $to_set['prerequisite'] = $request['prerequisite'];
            } else {
                $to_set['prerequisite'] = 0;
            }
        }

        // Needed until the following will be implemented: https://github.com/gocodebox/lifterlms/issues/908.
        $to_set['has_prerequisite'] = empty( $to_set['prerequisite'] ) ? 'no' : 'yes';

        // Set bulk.
        if ( ! empty( $to_set ) ) {
            $update = $lesson->set_bulk( $to_set, true, true );
            if ( is_wp_error( $update ) ) {
                $error = $update;
            }
        }

        if ( ! empty( $error->errors ) ) {
            return $error;
        }

        return ! empty( $to_set );
    }

    /**
     * Get the Lesson's schema, conforming to JSON Schema.
     *
     * @since 1.0.0-beta.27
     *
     * @return array Item schema data.
     */
    protected function get_item_schema_base() {

        $schema = (array) parent::get_item_schema_base();

        $lesson_properties = array(
            'parent_id'    => array(
                'description' => __( 'WordPress post ID of the parent item. Must be a Section ID. 0 indicates an "orphaned" lesson which can be edited and viewed by instructors and admins but cannot be read by students.', 'lifterlms' ),
                'type'        => 'integer',
                'context'     => array( 'view', 'edit' ),
                'arg_options' => array(
                    'sanitize_callback' => 'absint',
                ),
            ),
            'course_id'    => array(
                'description' => __( 'WordPress post ID of the lesson\'s parent course.', 'lifterlms' ),
                'type'        => 'integer',
                'context'     => array( 'view', 'edit' ),
                'arg_options' => array(
                    'sanitize_callback' => 'absint',
                ),
                'readonly'    => true,
            ),
            'order'        => array(
                'description' => __( 'Order of the lesson within its immediate parent.', 'lifterlms' ),
                'type'        => 'integer',
                'default'     => 1,
                'context'     => array( 'view', 'edit' ),
                'arg_options' => array(
                    'sanitize_callback' => 'absint',
                ),
                'required'    => true,
            ),
            'prerequisite' => array(
                'description' => __( 'Lesson ID of the prerequisite lesson.', 'lifterlms' ),
                'type'        => 'integer',
                'context'     => array( 'view', 'edit' ),
                'arg_options' => array(
                    'sanitize_callback' => 'absint',
                ),
            ),
            'points'       => array(
                'description' => __( 'Determines the weight of the lesson when grading the course.', 'lifterlms' ),
                'type'        => 'integer',
                'default'     => 1,
                'context'     => array( 'view', 'edit' ),
                'arg_options' => array(
                    'sanitize_callback' => 'absint',
                ),
            ),
            'audio_embed'  => array(
                'description' => __( 'URL to an oEmbed enable audio URL.', 'lifterlms' ),
                'type'        => 'string',
                'context'     => array( 'view', 'edit' ),
                'format'      => 'uri',
                'arg_options' => array(
                    'sanitize_callback' => 'esc_url_raw',
                ),
            ),
            'video_embed'  => array(
                'description' => __( 'URL to an oEmbed enable video URL.', 'lifterlms' ),
                'type'        => 'string',
                'context'     => array( 'view', 'edit' ),
                'format'      => 'uri',
                'arg_options' => array(
                    'sanitize_callback' => 'esc_url_raw',
                ),
            ),
            'drip_date'    => array(
                'description' => __(
                    'The date and time when the lesson becomes available. Applicable only when drip_method is date. Format: Y-m-d H:i:s.',
                    'lifterlms'
                ),
                'type'        => 'string',
                'context'     => array( 'view', 'edit' ),
            ),
            'drip_days'    => array(
                'description' => __( 'Number of days to wait before allowing access to the lesson. Applicable only when drip_method is enrollment, start, or prerequisite.', 'lifterlms' ),
                'type'        => 'integer',
                'default'     => 1,
                'context'     => array( 'view', 'edit' ),
                'arg_options' => array(
                    'sanitize_callback' => 'absint',
                ),
            ),
            'drip_method'  => array(
                'description' => __(
                    'Determine the method with which to make the lesson content available.
                    <ul>
                        <li>none: Drip is disabled; the lesson is immediately available.</li>
                        <li>date: Lesson is made available at a specific date and time.</li>
                        <li>enrollment: Lesson is made available a specific number of days after enrollment into the course.</li>
                        <li>start: Lesson is made available a specific number of days after the course\'s start date. Only available on courses with a access_opens_date.</li>
                        <li>prerequisite: Lesson is made available a specific number of days after the prerequisite lesson is completed.</li>
                    </ul>',
                    'lifterlms'
                ),
                'type'        => 'string',
                'default'     => 'none',
                'enum'        => array( 'none', 'date', 'enrollment', 'start', 'prerequisite' ),
                'context'     => array( 'view', 'edit' ),
            ),
            'public'       => array(
                'description' => __( 'Denotes a lesson that\'s publicly accessible regardless of course enrollment.', 'lifterlms' ),
                'type'        => 'boolean',
                'default'     => false,
                'context'     => array( 'view', 'edit' ),
            ),
            'quiz'         => array(
                'description' => __( 'Associate a quiz with this lesson.', 'lifterlms' ),
                'type'        => 'object',
                'context'     => array( 'view', 'edit' ),
                'arg_options' => array(
                    'sanitize_callback' => null, // Note: sanitization implemented in self::prepare_item_for_database().
                    'validate_callback' => null, // Note: validation implemented in self::prepare_item_for_database().
                ),
                'properties'  => array(
                    'enabled'     => array(
                        'description' => __( 'Determines if a quiz is enabled for the lesson.', 'lifterlms' ),
                        'type'        => 'boolean',
                        'default'     => false,
                        'context'     => array( 'view', 'edit' ),
                    ),
                    'id'          => array(
                        'description' => __( 'The post ID of the associated quiz.', 'lifterlms' ),
                        'type'        => 'integer',
                        'default'     => 0,
                        'context'     => array( 'view', 'edit' ),
                        'arg_options' => array(
                            'sanitize_callback' => 'absint',
                        ),
                    ),
                    'progression' => array(
                        'description' => __(
                            'Determines lesson progression requirements related to the quiz.
                            <ul>
                                <li>complete: The quiz must be completed (with any grade) to progress the lesson.</li>
                                <li>pass: A passing grade must be earned to progress the lesson.</li>
                            </ul>',
                            'lifterlms'
                        ),
                        'type'        => 'string',
                        'default'     => 'complete',
                        'enum'        => array( 'complete', 'pass' ),
                        'context'     => array( 'view', 'edit' ),
                    ),
                ),
            ),
        );

        $schema['properties'] = array_merge( (array) $schema['properties'], $lesson_properties );

        return $schema;
    }

    /**
     * Retrieves the query params for the objects collection.
     *
     * @since 1.0.0-beta.1
     *
     * @return array The Lessons collection parameters.
     */
    public function get_collection_params() {
        return $this->collection_params;
    }

    /**
     * Retrieves the query params for the objects collection.
     *
     * @since 1.0.0-beta.1
     *
     * @param array $collection_params The Lessons collection parameters to be set.
     * @return void
     */
    public function set_collection_params( $collection_params ) {
        $this->collection_params = $collection_params;
    }

    /**
     * Retrieves the query params for the objects collection.
     *
     * @since 1.0.0-beta.1
     *
     * @return array Collection parameters.
     */
    public function build_collection_params() {

        $query_params = parent::get_collection_params();

        $query_params['parent'] = array(
            'description'       => __( 'Filter lessons by the parent post (section) ID.', 'lifterlms' ),
            'type'              => 'integer',
            'validate_callback' => 'rest_validate_request_arg',
        );

        return $query_params;
    }

    /**
     * Prepare a single object output for response.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.7 Added following properties to the response object:
     *                     public, points, quiz, drip_method, drip_days, drip_date, prerequisite, audio_embed, video_embed.
     *                     Added `llms_rest_prepare_lesson_object_response` filter hook.
     * @since 1.0.0-beta.23 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.
     *                      public, points, quiz, drip_method, drip_days, drip_date, prerequisite, audio_embed, video_embed.
     *                      Added `llms_rest_prepare_lesson_object_response` filter hook.
     *
     * @param LLMS_Lesson     $lesson  Lesson object.
     * @param WP_REST_Request $request Full details about the request.
     * @return array
     */
    protected function prepare_object_for_response( $lesson, $request ) {

        $data = parent::prepare_object_for_response( $lesson, $request );

        // Audio Embed.
        $data['audio_embed'] = $lesson->get( 'audio_embed' );

        // Video Embed.
        $data['video_embed'] = $lesson->get( 'video_embed' );

        // Parent section.
        $data['parent_id'] = $lesson->get_parent_section();

        // Parent course.
        $data['course_id'] = $lesson->get( 'parent_course' );

        // Order.
        $data['order'] = $lesson->get( 'order' );

        // Public.
        $data['public'] = $lesson->is_free();

        // Points.
        $data['points'] = $lesson->get( 'points' );

        // Quiz.
        $data['quiz']['enabled']     = llms_parse_bool( $lesson->get( 'quiz_enabled' ) );
        $data['quiz']['id']          = absint( $lesson->get( 'quiz' ) );
        $data['quiz']['progression'] = llms_parse_bool( $lesson->get( 'require_passing_grade' ) ) ? 'pass' : 'completed';

        // Drip method.
        $data['drip_method'] = $lesson->get( 'drip_method' );
        $data['drip_method'] = $data['drip_method'] ? $data['drip_method'] : 'none';

        // Drip days.
        $data['drip_days'] = absint( $lesson->get( 'days_before_available' ) );

        // Drip date.
        $date = $lesson->get( 'date_available' );
        if ( $date ) {
            $time = $lesson->get( 'time_available' );

            if ( ! $time ) {
                $time = '12:00 AM';
            }

            $drip_date = date_i18n( 'Y-m-d H:i:s', strtotime( $date . ' ' . $time ) );
        } else {
            $drip_date = '';
        }

        $data['drip_date'] = $drip_date;

        // Prerequisite.
        $data['prerequisite'] = absint( $lesson->get_prerequisite() );

        /**
         * Filters the lesson data for a response.
         *
         * @since 1.0.0-beta.7
         *
         * @param array           $data    Array of lesson properties prepared for response.
         * @param LLMS_Lesson     $lesson  Lesson object.
         * @param WP_REST_Request $request Full details about the request.
         */
        return apply_filters( 'llms_rest_prepare_lesson_object_response', $data, $lesson, $request );
    }

    /**
     * Format query arguments to retrieve a collection of objects.
     *
     * @since 1.0.0-beta.7
     * @since 1.0.0-beta.12 Updated to reflect changes in the parent class.
     * @since 1.0.0-beta.18 Correctly return errors.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return array|WP_Error
     */
    protected function prepare_collection_query_args( $request ) {

        $query_args = parent::prepare_collection_query_args( $request );
        if ( is_wp_error( $query_args ) ) {
            return $query_args;
        }

        // Orderby 'order' requires a meta query.
        if ( isset( $query_args['orderby'] ) && 'order' === $query_args['orderby'] ) {
            $query_args = array_merge(
                $query_args,
                array(
                    'meta_key' => '_llms_order',
                    'orderby'  => 'meta_value_num',
                )
            );
        }

        if ( isset( $this->parent_id ) ) {
            $parent_id = $this->parent_id;
        } elseif ( ! empty( $request['parent'] ) && $request['parent'] > 1 ) {
            $parent_id = $request['parent'];
        }

        // Filter by parent.
        if ( ! empty( $parent_id ) ) {
            $query_args = array_merge(
                $query_args,
                array(
                    'meta_query' => array(
                        array(
                            'key'     => '_llms_parent_section',
                            'value'   => $parent_id,
                            'compare' => '=',
                        ),
                    ),
                )
            );
        }

        return $query_args;
    }

    /**
     * Get action/filters to be removed before preparing the item for response.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.7 Fixed lesson progression callback name.
     * @since 1.0.0-beta.9 `llms_rest_lesson_filters_removed_for_response` filter hook added.
     *
     * @param LLMS_Lesson $lesson Lesson object.
     * @return array Array of action/filters to be removed for response.
     */
    protected function get_filters_to_be_removed_for_response( $lesson ) {

        $filters = array();

        if ( llms_blocks_is_post_migrated( $lesson->get( 'id' ) ) ) {
            $filters = array(
                // hook => [callback, priority].
                'lifterlms_single_lesson_after_summary' => array(
                    // Lesson Navigation.
                    array(
                        'callback' => 'lifterlms_template_lesson_navigation',
                        'priority' => 20,
                    ),
                    // Lesson Progression.
                    array(
                        'callback' => 'lifterlms_template_complete_lesson_link',
                        'priority' => 10,
                    ),
                ),
            );
        }

        /**
         * Modify the array of filters to be removed before building the response.
         *
         * @since 1.0.0-beta.9
         *
         * @param array       $filters Array of filters to be removed.
         * @param LLMS_Lesson $lesson  Lesson object.
         */
        return apply_filters( 'llms_rest_lesson_filters_removed_for_response', $filters, $lesson );
    }

    /**
     * Prepare links for the request.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.7 Fixed `siblings` link that was using the parent course's id instead of the parent section's id.
     *                  Fixed `parent` link href, replacing 'section' with 'sections'.
     *                  Following links added: `prerequisite`, `quiz`.
     *                  Added `llms_rest_lesson_links` filter hook.
     * @since 1.0.0-beta.14 Added `$request` parameter.
     * @since 1.0.0-beta.23 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.
     *
     * @param LLMS_Lesson     $lesson  LLMS Section.
     * @param WP_REST_Request $request Request object.
     * @return array Links for the given object.
     */
    protected function prepare_links( $lesson, $request ) {

        $links = parent::prepare_links( $lesson, $request );

        unset( $links['content'] );

        $lesson_id         = $lesson->get( 'id' );
        $parent_course_id  = $lesson->get( 'parent_course' );
        $parent_section_id = $lesson->get_parent_section();

        $lesson_links = array();

        // Parent course.
        if ( $parent_course_id ) {
            $lesson_links['course'] = array(
                'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'courses', $parent_course_id ) ),
            );
        }

        // Parent section.
        if ( $parent_section_id ) {
            $lesson_links['parent'] = array(
                'type' => 'section',
                'href' => rest_url( sprintf( '/%s/%s/%d', 'llms/v1', 'sections', $parent_section_id ) ),
            );
        }

        // Siblings.
        $lesson_links['siblings'] = array(
            'href' => add_query_arg(
                'parent',
                $parent_section_id,
                $links['collection']['href']
            ),
        );

        // Next.
        $next_lesson = $lesson->get_next_lesson();
        if ( $next_lesson ) {
            $lesson_links['next'] = array(
                'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $next_lesson ) ),
            );
        }

        // Previous.
        $previous_lesson = $lesson->get_previous_lesson();
        if ( $previous_lesson ) {
            $lesson_links['previous'] = array(
                'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $previous_lesson ) ),
            );
        }

        // Prerequisite.
        $prerequisite = $lesson->get_prerequisite();

        if ( ! empty( $prerequisite ) ) {
            $lesson_links['prerequisite'] = array(
                'type' => $this->post_type,
                'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $prerequisite ) ),
            );
        }

        $links = array_merge( $links, $lesson_links );

        /**
         * Filters the lesson's links.
         *
         * @since 1.0.0-beta.7
         *
         * @param array       $links  Links for the given lesson.
         * @param LLMS_Lesson $lesson Lesson object.
         */
        return apply_filters( 'llms_rest_lesson_links', $links, $lesson );
    }

    /**
     * Checks if a Lesson can be read
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.23 Replaced the call to the deprecated `LLMS_Lesson::get_parent_course()` method with `LLMS_Lesson::get( 'parent_course' )`.
     *
     * @param LLMS_Lesson $lesson The Lesson object.
     * @return bool Whether the post can be read.
     *
     * @todo Implement read permission based on the section's id:
     * 0 indicates an "orphaned" lesson which can be edited and viewed by instructors and admins but cannot be read by students.
     */
    protected function check_read_permission( $lesson ) {

        /**
         * As of now, lessons of password protected courses cannot be read
         */
        if ( post_password_required( $lesson->get( 'parent_course' ) ) ) {
            return false;
        }

        /**
         * At the moment we grant lessons read permission only to who can edit lessons.
         */
        return parent::check_update_permission( $lesson );
    }
}