gocodebox/lifterlms-rest

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

Summary

Maintainability
F
4 days
Test Coverage
A
93%
<?php
/**
 * REST Enrollments Controller.
 *
 * @package LLMS_REST
 *
 * @since 1.0.0-beta.1
 * @version 1.0.0-beta.27
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_REST_Enrollments_Controller class.
 *
 * @since 1.0.0-beta.1
 * @since 1.0.0-beta.3 Don't output "Last" page link header on the last page.
 * @since 1.0.0-beta.4 Everybody who can view the enrollment's student can list the enrollments although the single
 *                     enrollment permission will be checked in `LLMS_REST_Enrollments_Controller::get_objects()`.
 *                     The single enrollment can be read only by who can view the enrollment's student.
 *                     Enrollment's post_id and student_id casted to integer, and fix calling
 *                     to some undefined functions.
 * @since 1.0.0-beta.7 `prepare_objects_query()` renamed to `prepare_collection_query_args()`.
 *                     `prepare_object_query()` renamed to `prepare_object_query_args()`.
 *                     Added: `get_objects_from_query()`, `prepare_objects_query()`,
 *                     `get_pagination_data_from_query()`, `prepare_collection_items_for_response()`
 *                     methods overrides.
 *                     `get_items()` method removed, now abstracted in LLMS_REST_Controller.
 *                     Fixed description of the `post_id` path parameter.
 * @since 1.0.0-beta.10 Added `trigger` property and as param for creation/update/and deletion requests.
 *                     Added `get_endpoint_args_for_item_schema()` method override.
 *                     Use backticks in args and item schema properties descriptions where convenient.
 *                     Filter prepared enrollment for response in order to include only fields available for response.
 *                     Added `llms_rest_enrollments_item_schema`, `llms_rest_prepare_enrollment_object_response`,
 *                     `llms_rest_enrollment_links` filter hooks.
 *                     Also fix return when the enrollment to be deleted doesn't exist.
 *                     Fixed 'context' query parameter schema.
 * @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_Enrollments_Controller extends LLMS_REST_Controller {

    /**
     * Route base.
     *
     * @var string
     */
    protected $rest_base = 'students/(?P<id>[\d]+)/enrollments';

    /**
     * Collection params.
     *
     * @var array()
     */
    protected $collection_params;

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

    /**
     * Constructor.
     *
     * @since 1.0.0-beta.1
     */
    public function __construct() {
        $this->collection_params = $this->build_collection_params();
    }

    /**
     * Retrieves an array of endpoint arguments from the item schema for the controller.
     *
     * @since 1.0.0-beta.10
     *
     * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are
     *                       checked for required values and may fall-back to a given default, this is not done
     *                       on `EDITABLE` requests. Default WP_REST_Server::CREATABLE.
     * @return array Endpoint arguments.
     */
    public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) {

        if ( in_array( $method, array( 'PATCH', 'POST', WP_REST_Server::DELETABLE ), true ) ) {
            $args = array(
                'trigger' => array(
                    'description'       => __( 'The trigger of the enrollment to act on.', 'lifterlms' ),
                    'type'              => 'string',
                    'default'           => 'any',
                    'sanitize_callback' => 'sanitize_text_field',
                    'validate_callback' => 'rest_validate_request_arg',
                ),
            );
        } else {
            $args = parent::get_endpoint_args_for_item_schema( $method );
        }

        return $args;

    }

    /**
     * Register routes.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.7 Fixed description of the `post_id` path parameter.
     * @since 1.0.0-beta.10 Add `trigger` param for create/update/delete endpoints.
     *                      Use backticks in args descriptions.
     *
     * @return void
     */
    public function register_routes() {

        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base,
            array(
                array(
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => array( $this, 'get_items' ),
                    'permission_callback' => array( $this, 'get_items_permissions_check' ),
                    'args'                => $this->get_collection_params(),
                ),
                'schema' => array( $this, 'get_public_item_schema' ),
            )
        );

        register_rest_route(
            $this->namespace,
            '/' . $this->rest_base . '/(?P<post_id>[\d]+)',
            array(
                'args'   => array(
                    'post_id' => array(
                        'description' => __( 'Unique course or membership Identifier. The WordPress Post `ID.`', 'lifterlms' ),
                        'type'        => 'integer',
                    ),
                ),
                array(
                    'methods'             => WP_REST_Server::READABLE,
                    'callback'            => array( $this, 'get_item' ),
                    'permission_callback' => array( $this, 'get_item_permissions_check' ),
                    'args'                => $this->get_get_item_params(),
                ),
                array(
                    'methods'             => 'POST',
                    'callback'            => array( $this, 'create_item' ),
                    'permission_callback' => array( $this, 'create_item_permissions_check' ),
                    'args'                => $this->get_endpoint_args_for_item_schema( 'POST' ),
                ),
                array(
                    'methods'             => 'PATCH',
                    'callback'            => array( $this, 'update_item' ),
                    'permission_callback' => array( $this, 'update_item_permissions_check' ),
                    'args'                => $this->get_endpoint_args_for_item_schema( 'PATCH' ),
                ),
                array(
                    'methods'             => WP_REST_Server::DELETABLE,
                    'callback'            => array( $this, 'delete_item' ),
                    'permission_callback' => array( $this, 'delete_item_permissions_check' ),
                    'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::DELETABLE ),
                ),
                'schema' => array( $this, 'get_public_item_schema' ),
            )
        );

    }

    /**
     * Check if a given request has access to read items.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.4 Everybody who can view the enrollment's student can list the enrollments although
     *                     the single enrollment permission will be checked in
     *                     `LLMS_REST_Enrollments_Controller::get_objects()`.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function get_items_permissions_check( $request ) {

        if ( stristr( $request->get_route(), '/students/' ) && isset( $request['id'] ) ) {
            $enrollment             = new stdClass();
            $enrollment->student_id = (int) $request['id'];
            if ( ! $this->check_read_permission( $enrollment ) ) {
                return llms_rest_authorization_required_error();
            }
        }

        return true;

    }

    /**
     * Get a collection of enrollments.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.3 Don't output "Last" page link header on the last page.
     * @since 1.0.0-beta.7 Overrides the parent `get_items()` for the only purpose of returning a 404 if no enrollments are found.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_items( $request ) {

        $response = parent::get_items( $request );
        // Specs require 404 when no course enrollments are found.
        if ( ! is_wp_error( $response ) && empty( $response->data ) ) {
            return llms_rest_not_found_error();
        }

        return $response;

    }

    /**
     * Check if a given request has access to read an item.
     *
     * @since 1.0.0-beta.1
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function get_item_permissions_check( $request ) {

        $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'] );
        if ( is_wp_error( $enrollment_exists ) ) {
            return $enrollment_exists;
        }

        $object = new stdClass();

        $object->student_id = (int) $request['id'];
        $object->post_id    = (int) $request['post_id'];

        if ( ! $this->check_read_permission( $object ) ) {
            return llms_rest_authorization_required_error();
        }

        return true;
    }

    /**
     * Get a single item.
     *
     * @since 1.0.0-beta.1
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_Error|WP_REST_Response
     */
    public function get_item( $request ) {

        $object = $this->get_object( (int) $request['id'], (int) $request['post_id'] );
        if ( is_wp_error( $object ) ) {
            return $object;
        }

        $response = $this->prepare_item_for_response( $object, $request );

        return $response;

    }

    /**
     * Check if a given request has access to create an item.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 Handle the `trigger` param.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function create_item_permissions_check( $request ) {

        $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'], $request['trigger'], false );

        if ( $enrollment_exists ) {
            return llms_rest_bad_request_error( __( 'Cannot create existing enrollment. Use the PATCH method if you want to update an existing enrollment', 'lifterlms' ) );
        }

        if ( ! $this->check_create_permission() ) {
            return llms_rest_authorization_required_error( __( 'Sorry, you are not allowed to create an enrollment as this user.', 'lifterlms' ) );
        }

        return true;
    }


    /**
     * Creates a single enrollment.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 Handle the `trigger` param.
     * @since 1.0.0-beta.26 By default don't load the current user if a falsy student ID is supplied.
     * @since 1.0.0-beta.27 Handle custom rest fields registered via `register_rest_field()`.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function create_item( $request ) {

        $user_id = (int) $request['id'];
        $post_id = (int) $request['post_id'];

        // The default trigger for the `LLMS_Student::enroll()` method is 'unspecified'.
        $trigger = $request['trigger'] && 'any' !== $request['trigger'] ? $request['trigger'] : 'unspecified';

        // Check both students and product exist.
        $student = new LLMS_Student( $user_id, false );

        if ( ! $student->exists() ) {
            return llms_rest_not_found_error();
        }

        // Can only be enrolled in the following post types.
        $product_type = get_post_type( $post_id );
        if ( ! $product_type ) {
            return llms_rest_not_found_error();
        }

        if ( ! in_array( $product_type, array( 'course', 'llms_membership' ), true ) ) {
            return llms_rest_bad_request_error();
        }

        // Enroll.
        $enroll = $student->enroll( $post_id, $trigger );

        // Something went wrong internally.
        if ( ! $enroll ) {
            return llms_rest_server_error( __( 'The enrollment could not be created', 'lifterlms' ) );
        }

        $request->set_param( 'context', 'edit' );
        $enrollment = $this->get_object( $user_id, $post_id );

        // Fields registered via `register_rest_field()`.
        $fields_update = $this->update_additional_fields_for_object( $enrollment, $request );
        if ( is_wp_error( $fields_update ) ) {
            return $fields_update;
        }

        $response = $this->prepare_item_for_response( $enrollment, $request );

        $response->set_status( 201 );

        $response->header(
            'Location',
            rest_url( sprintf( '/%s/%s/%d/%s/%d', 'llms/v1', 'students', $enrollment->student_id, 'enrollments', $enrollment->post_id ) )
        );

        return $response;

    }

    /**
     * Check if a given request has access to update an item.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 Handle the `trigger` param.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function update_item_permissions_check( $request ) {

        $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'], $request['trigger'] );
        if ( is_wp_error( $enrollment_exists ) ) {
            return $enrollment_exists;
        }

        if ( ! $this->check_update_permission() ) {
            return llms_rest_authorization_required_error( __( 'Sorry, you are not allowed to update an enrollment as this user.', 'lifterlms' ) );
        }

        return true;

    }

    /**
     * Update item.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.4 Return a bad request error when supplying an invalid date_created param.
     * @since 1.0.0-beta.10 Handle `trigger` param.
     * @since 1.0.0-beta.26 By default don't load the current user if a falsy student ID is supplied.
     * @since 1.0.0-beta.27 Handle custom rest fields registered via `register_rest_field()`.
     *
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response|WP_Error Response object or WP_Error on failure.
     */
    public function update_item( $request ) {

        $student_id = (int) $request['id'];
        $post_id    = (int) $request['post_id'];

        // Check both students and product exist.
        $student = new LLMS_Student( $student_id, false );

        if ( ! $student->exists() ) {
            return llms_rest_not_found_error();
        }

        // Can only be enrolled in the following post types.
        $product_type = get_post_type( $post_id );
        if ( ! $product_type ) {
            return llms_rest_not_found_error();
        }
        if ( ! in_array( $product_type, array( 'course', 'llms_membership' ), true ) ) {
            return llms_rest_bad_request_error();
        }

        if ( 'any' !== $request['trigger'] && $request['trigger'] !== $student->get_enrollment_trigger( $post_id ) ) {
            return llms_rest_not_found_error();
        }

        $schema = $this->get_item_schema();

        if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {

            $updated_status = $this->handle_status_update( $student, $post_id, $request['status'], $request['trigger'] );

            // Something went wrong internally.
            if ( ! $updated_status ) {
                return llms_rest_server_error( __( 'The enrollment status could not be updated', 'lifterlms' ) );
            }
        }

        if ( ! empty( $schema['properties']['date_created'] ) && isset( $request['date_created'] ) ) {

            $updated_date_created = $this->handle_creation_date_update( $student_id, $post_id, $request['date_created'] );

            if ( is_wp_error( $updated_date_created ) ) {
                return $updated_date_created;
            }

            // Something went wrong internally.
            if ( ! $updated_date_created ) {
                return llms_rest_server_error( __( 'The enrollment creation date could not be updated', 'lifterlms' ) );
            }
        }

        $request->set_param( 'context', 'edit' );

        $enrollment = $this->get_object( $student_id, $post_id );

        // Fields registered via `register_rest_field()`.
        $fields_update = $this->update_additional_fields_for_object( $enrollment, $request );
        if ( is_wp_error( $fields_update ) ) {
            return $fields_update;
        }

        $response = $this->prepare_item_for_response( $enrollment, $request );

        return $response;

    }

    /**
     * Check if a given request has access to delete an item.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 The`trigger` param is now taken into account.
     * @since 1.0.0-beta.18 Provide a more significant error message when trying to delete an item without permissions.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function delete_item_permissions_check( $request ) {

        $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'], $request['trigger'] );
        if ( is_wp_error( $enrollment_exists ) ) {
            // Enrollment not found, we don't return a 404.
            if ( in_array( 'llms_rest_not_found', $enrollment_exists->get_error_codes(), true ) ) {
                return true;
            }

            return $enrollment_exists;
        }

        if ( ! $this->check_delete_permission() ) {
            return llms_rest_authorization_required_error(
                sprintf(
                    // Translators: %s = The post type name.
                    __( 'Sorry, you are not allowed to delete enrollments as this user.', 'lifterlms' ),
                    get_post_type_object( $this->post_type )->labels->name
                )
            );

        }

        return true;

    }

    /**
     * Deletes a single llms post.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 The`trigger` param is now taken into account.
     *                     Also fix return when the enrollment to be deleted doesn't exist.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
     */
    public function delete_item( $request ) {

        $response = new WP_REST_Response();
        $response->set_status( 204 );

        $enrollment_exists = $this->enrollment_exists( (int) $request['id'], (int) $request['post_id'], $request['trigger'] );

        if ( is_wp_error( $enrollment_exists ) ) {
            // Enrollment not found, we don't return a 404.
            if ( in_array( 'llms_rest_not_found', $enrollment_exists->get_error_codes(), true ) ) {
                return $response;
            }

            return $enrollment_exists;
        }

        $result = llms_delete_student_enrollment( (int) $request['id'], (int) $request['post_id'], $request['trigger'] );

        if ( ! $result ) {
            return llms_rest_server_error( __( 'The enrollment cannot be deleted.', 'lifterlms' ) );
        }

        return rest_ensure_response( $response );

    }

    /**
     * Check enrollment existence.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 Added the `trigger` param.
     * @since 1.0.0-beta.26 By default don't load the current user if a falsy student ID is supplied.
     *
     * @param int     $student_id Student ID.
     * @param int     $post_id    The course/membership ID.
     * @param string  $trigger    Optional. The enrollment trigger. Default 'any'.
     * @param boolean $wp_error   Optional. Whether return a WP_Error instance or a boolean. Default true (returns WP_Error).
     * @return WP_Error|boolean
     */
    protected function enrollment_exists( $student_id, $post_id, $trigger = 'any', $wp_error = true ) {

        $student = llms_get_student( $student_id, false );

        if ( empty( $student ) ) {
            return $wp_error ? llms_rest_not_found_error() : false;
        }

        $current_status = $student->get_enrollment_status( $post_id );

        if ( empty( $current_status ) ) {
            return $wp_error ? llms_rest_not_found_error() : false;
        }

        if ( 'any' !== $trigger && $trigger !== $student->get_enrollment_trigger( $post_id ) ) {
            return $wp_error ? llms_rest_not_found_error() : false;
        }

        return true;

    }

    /**
     * Get object.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.4 Fix call to undefined function llms_rest_bad_request(),
     *                     must be llms_rest_bad_request_error().
     *
     * @param int $student_id Student ID.
     * @param int $post_id The course/membership ID.
     * @return object|WP_Error
     */
    protected function get_object( $student_id, $post_id = null ) {

        if ( empty( $post_id ) ) {
            return llms_rest_bad_request_error();
        }

        $query_args = $this->prepare_object_query_args( $student_id, $post_id );
        $query      = $this->get_objects_query( $query_args );
        $items      = $this->get_objects_from_query( $query );

        if ( $items ) {
            return $items[0];
        }

        return llms_rest_not_found_error();
    }

    /**
     * Prepare enrollments objects query.
     *
     * @since 1.0.0-beta.7
     * @since 1.0.0-beta.10 Set query limit to 1.
     *
     * @param int $student_id Student ID.
     * @param int $post_id The course/membership ID.
     * @return array
     */
    protected function prepare_object_query_args( $student_id, $post_id ) {

        $args = array();

        $args['id']            = $student_id;
        $args['post']          = $post_id;
        $args['no_found_rows'] = true;
        $args['per_page']      = 1;

        $args = $this->prepare_items_query( $args );

        return $args;

    }

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

    /**
     * Build the query params for the objects collection.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 Fixed 'context' query parameter schema.
     *
     * @return array Collection parameters.
     */
    protected function build_collection_params() {

        $query_params = parent::get_collection_params();

        unset( $query_params['include'], $query_params['exclude'] );

        $query_params['status'] = array(
            'description'       => __( 'Filter results to records matching the specified status.', 'lifterlms' ),
            'enum'              => array_keys( llms_get_enrollment_statuses() ),
            'type'              => 'string',
            'validate_callback' => 'rest_validate_request_arg',
        );

        $query_params['post'] = array(
            'description'       => __( 'Limit results to a specific course or membership or a list of courses and/or memberships. Accepts a single post id or a comma separated list of post ids.', 'lifterlms' ),
            'type'              => 'string',
            'validate_callback' => 'rest_validate_request_arg',
        );

        return $query_params;
    }

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

        $schema = array(
            '$schema'    => 'http://json-schema.org/draft-04/schema#',
            'title'      => 'students-enrollments',
            'type'       => 'object',
            'properties' => array(
                'post_id'      => array(
                    'description' => __( 'The ID of the course/membership.', 'lifterlms' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'student_id'   => array(
                    'description' => __( 'The ID of the student.', 'lifterlms' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'date_created' => array(
                    'description' => __( 'Creation date. Format: `Y-m-d H:i:s`', 'lifterlms' ),
                    'type'        => 'string',
                    'context'     => array( 'view', 'edit' ),
                ),
                'date_updated' => array(
                    'description' => __( 'Date last modified. Format: `Y-m-d H:i:s`', 'lifterlms' ),
                    'type'        => 'string',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'status'       => array(
                    'description' => __( 'The status of the enrollment.', 'lifterlms' ),
                    'enum'        => array_keys( llms_get_enrollment_statuses() ),
                    'context'     => array( 'view', 'edit' ),
                    'type'        => 'string',
                ),
                'trigger'      => array(
                    'description' => __( 'The enrollment trigger. Default is `any`.', 'lifterlms' ),
                    'context'     => array( 'view', 'edit' ),
                    'type'        => 'string',
                    'default'     => 'any',
                    'readonly'    => true,
                ),
            ),
        );

        $object_type = $this->get_object_type( $schema );

        /**
         * Filter item schema for the enrollments controller.
         *
         * @since 1.0.0-beta.10
         * @deprecated 1.0.0-beta.27
         *
         * @param array $schema Item schema data.
         */
        return apply_filters_deprecated(
            'llms_rest_enrollments_item_schema',
            array( $schema ),
            '[version]',
            "llms_rest_{$this->get_object_type( $schema )}_item_schema"
        );

    }

    /**
     * Retrieve an array of objects from the result of $this->get_objects_query().
     *
     * @since 1.0.0-beta.7
     *
     * @param WP_Query $query Query result.
     * @return obj[]
     */
    protected function get_objects_from_query( $query ) {

        return $query->items;

    }

    /**
     * Prepare collection items for response.
     *
     * @since 1.0.0-beta.7
     *
     * @param array           $objects Array of objects to be prepared for response.
     * @param WP_REST_Request $request Full details about the request.
     * @return array
     */
    protected function prepare_collection_items_for_response( $objects, $request ) {

        $items = array();

        foreach ( $objects as $object ) {

            if ( ! $this->check_read_permission( $object ) ) {
                continue;
            }

            $item = $this->prepare_item_for_response( $object, $request );
            if ( ! is_wp_error( $item ) ) {
                $items[] = $this->prepare_response_for_collection( $item );
            }
        }

        return $items;
    }

    /**
     * Retrieve pagination information from an objects query.
     *
     * @since 1.0.0-beta.7
     *
     * @param stdClass        $query    Objects query result returned by {@see LLMS_REST_Enrollments_Controller::get_objects_query()}.
     * @param array           $prepared Array of collection arguments.
     * @param WP_REST_Request $request  Request object.
     * @return array {
     *     Array of pagination information.
     *
     *     @type int $current_page  Current page number.
     *     @type int $total_results Total number of results.
     *     @type int $total_pages   Total number of results pages.
     * }
     */
    protected function get_pagination_data_from_query( $query, $prepared, $request ) {

        $total_results = (int) $query->found_results;
        $current_page  = isset( $prepared['page'] ) ? (int) $prepared['page'] : 1;
        $total_pages   = (int) ceil( $total_results / (int) $prepared['per_page'] );

        return compact( 'current_page', 'total_results', 'total_pages' );

    }

    /**
     * Prepare enrollments objects query
     *
     * @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 ) {

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

        $prepared['id']   = $request['id'];
        $prepared['page'] = ! isset( $prepared['page'] ) ? 1 : $prepared['page'];

        return $this->prepare_items_query( $prepared, $request );

    }

    /**
     * Determines the allowed query_vars for a get_items() response and prepares
     * them for WP_Query.
     *
     * @since 1.0.0-beta.1
     *
     * @param array           $prepared_args Optional. Prepared WP_Query arguments. Default empty array.
     * @param WP_REST_Request $request       Optional. Full details about the request.
     * @return array Items query arguments.
     */
    protected function prepare_items_query( $prepared_args = array(), $request = null ) {

        $query_args = array();

        foreach ( $prepared_args as $key => $value ) {
            $query_args[ $key ] = $value;
        }

        // Filters.
        if ( isset( $query_args['student'] ) && ! is_array( $query_args['student'] ) ) {
            $query_args['student'] = array_map( 'absint', explode( ',', $query_args['student'] ) );
        }
        if ( isset( $query_args['post'] ) && ! is_array( $query_args['post'] ) ) {
            $query_args['post'] = array_map( 'absint', explode( ',', $query_args['post'] ) );
        }

        if ( isset( $query_args['orderby'] ) ) {
            switch ( $query_args['orderby'] ) {
                case 'date_updated':
                    $query_args['orderby'] = 'upm2.updated_date';
                    break;
                case 'date_created':
                    $query_args['orderby'] = 'upm.updated_date';
                    break;
                default:
                    unset( $query_args['orderby'] );
                    break;
            }
        }

        $query_args['is_students_route'] = $request ? false !== stristr( $request->get_route(), '/students/' ) : true;

        return $query_args;

    }

    /**
     * Get enrollments query.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.4 Enrollment's post_id and student_id casted to integer.
     * @since 1.0.0-beta.10 Added subquery to retrieve the enrollments trigger.
     * @since 1.0.0-beta.18 Fixed wrong trigger retrieved when multiple trigger were present for the same user,post pair.
     *
     * @param  array           $query_args Array of collection arguments.
     * @param  WP_REST_Request $request    Optional. Full details about the request. Default null.
     * @return stdClass An object with two fields: 'items' an array of OBJECT result of the query; 'found_results' the total found items.
     */
    protected function get_objects_query( $query_args, $request = null ) {

        global $wpdb;

        // Maybe limit the query results depending on the page param.
        if ( isset( $query_args['page'] ) ) {
            $skip  = $query_args['page'] > 1 ? ( $query_args['page'] - 1 ) * $query_args['per_page'] : 0;
            $limit = $wpdb->prepare(
                'LIMIT %d, %d',
                array(
                    $skip,
                    $query_args['per_page'],
                )
            );
        } else {
            $limit = $wpdb->prepare(
                'LIMIT %d',
                $query_args['per_page']
            );
        }

        /**
         * List enrollments of the current student_id or post_id.
         * Depends on the endpoint route.
         */
        if ( $query_args['is_students_route'] ) {
            $id_column = 'user_id';
        } else {
            $id_column = 'post_id';
        }

        /**
         * Filter the enrollments by user_id or post_id param
         */
        if ( isset( $query_args['student'] ) ) {
            $filter = sprintf( ' AND upm.user_id IN ( %s )', implode( ', ', $query_args['student'] ) );
        } elseif ( isset( $query_args['post'] ) ) {
            $filter = sprintf( ' AND upm.post_id IN ( %s )', implode( ', ', $query_args['post'] ) );
        } else {
            $filter = '';
        }

        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $updated_date_status = $wpdb->prepare(
            "(
                SELECT DISTINCT user_id, post_id, updated_date, meta_value
                FROM {$wpdb->prefix}lifterlms_user_postmeta as upm
                WHERE upm.{$id_column} = %d
                $filter AND upm.meta_key = '_status'
                AND upm.updated_date = (
                    SELECT MAX( upm2.updated_date )
                    FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm2
                    WHERE upm2.meta_key = '_status'
                    AND upm2.post_id = upm.post_id
                    AND upm2.user_id = upm.user_id
                )
            )",
            array(
                $query_args['id'],
            )
        );

        // Trigger.
        // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
        $trigger = $wpdb->prepare(
            "(
                SELECT DISTINCT user_id, post_id, meta_value
                FROM {$wpdb->prefix}lifterlms_user_postmeta as upm
                WHERE upm.{$id_column} = %d
                $filter AND upm.meta_key = '_enrollment_trigger'
                AND upm.updated_date = (
                    SELECT MAX( upm2.updated_date )
                    FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm2
                    WHERE upm2.meta_key = '_enrollment_trigger'
                    AND upm2.post_id = upm.post_id
                    AND upm2.user_id = upm.user_id
                )
            )",
            array(
                $query_args['id'],
            )
        );

        if ( isset( $query_args['status'] ) ) {
            $filter .= $wpdb->prepare( ' AND upm2.meta_value = %s', $query_args['status'] );
        }

        if ( isset( $query_args['orderby'], $query_args['order'] ) ) {
            $order = sprintf( 'ORDER BY %1$s %2$s', esc_sql( $query_args['orderby'] ), esc_sql( $query_args['order'] ) );
        } else {
            $order = '';
        }

        $query = new stdClass();

        $select_found_rows = empty( $query_args['no_found_rows'] ) ? esc_sql( 'SQL_CALC_FOUND_ROWS' ) : '';

        // the query.
        $query->items = $wpdb->get_results(
            $wpdb->prepare(
                "
                SELECT {$select_found_rows} DISTINCT upm.post_id AS post_id, upm.user_id as student_id, upm.updated_date as date_created, upm2.updated_date as date_updated, upm2.meta_value as status, upm3.meta_value as etrigger
                FROM {$wpdb->prefix}lifterlms_user_postmeta AS upm
                JOIN {$updated_date_status} as upm2 ON upm.post_id = upm2.post_id AND upm.user_id = upm2.user_id
                JOIN {$trigger} as upm3 ON upm.post_id = upm3.post_id AND upm.user_id = upm3.user_id
                WHERE upm.meta_key = '_start_date'
                AND upm.{$id_column} = %d
                {$filter}
                {$order}
                {$limit};
                ",
                array(
                    $query_args['id'],
                )
            )
        );// no-cache ok.
        // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared

        $count = count( $query->items );

        if ( $count ) {
            foreach ( $query->items as $key => $item ) {
                $query->items[ $key ]->post_id    = (int) $item->post_id;
                $query->items[ $key ]->student_id = (int) $item->student_id;
                $query->items[ $key ]->trigger    = (string) $item->etrigger;
                unset( $query->items[ $key ]->etrigger );
            }
        }

        $query->found_results = empty( $query_args['no_found_rows'] ) ? absint( $wpdb->get_var( 'SELECT FOUND_ROWS()' ) ) : $count; // no-cache ok.

        return $query;

    }

    /**
     * Prepare a single object output for response.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 Filter enrollment to include only fields available for response.
     *                      Added `llms_rest_prepare_enrollment_object_response` filter hook.
     *
     * @param stdClass        $enrollment Enrollment object.
     * @param WP_REST_Request $request Full details about the request.
     * @return array
     */
    public function prepare_object_for_response( $enrollment, $request ) {

        $prepared_enrollment = get_object_vars( $enrollment );

        // Apply filters.
        $prepared_enrollment['status'] = apply_filters(
            'llms_get_enrollment_status',
            $prepared_enrollment['status'],
            $prepared_enrollment['student_id'],
            $prepared_enrollment['post_id']
        );

        // Filter data including only schema props.
        $data = array_intersect_key( $prepared_enrollment, array_flip( $this->get_fields_for_response( $request ) ) );

        /**
         * Filters the enrollment data for a response.
         *
         * @since 1.0.0-beta.10
         *
         * @param array           $data       Array of enrollment properties prepared for response.
         * @param stdClass        $enrollment Enrollment object.
         * @param WP_REST_Request $request    Full details about the request.
         */
        return apply_filters( 'llms_rest_prepare_enrollment_object_response', $data, $enrollment, $request );
    }

    /**
     * Prepare enrollments links for the request.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.14 Added $request parameter.
     *
     * @param object          $enrollment Enrollment object data.
     * @param WP_REST_Request $request    Request object.
     * @return array Links for the given object.
     */
    public function prepare_links( $enrollment, $request ) {

        $links = array(
            'self'       => array(
                'href' => rest_url(
                    sprintf( '/%s/%s/%d/%s/%d', 'llms/v1', 'students', $enrollment->student_id, 'enrollments', $enrollment->post_id )
                ),
            ),
            'collection' => array(
                'href' => rest_url(
                    sprintf( '/%s/%s/%d/%s', 'llms/v1', 'students', $enrollment->student_id, 'enrollments' )
                ),
            ),
            'student'    => array(
                'href' => rest_url(
                    sprintf( '/%s/%s/%d', 'llms/v1', 'students', $enrollment->student_id )
                ),
            ),
        );

        switch ( get_post_type( $enrollment->post_id ) ) :
            case 'course':
                $links['post'] = array(
                    'type' => 'course',
                    'href' => rest_url(
                        sprintf( '/%s/%s/%d', 'llms/v1', 'courses', $enrollment->post_id )
                    ),
                );
                break;

            case 'llms_membership':
                $links['post'] = array(
                    'type' => 'llms_membership',
                    'href' => rest_url(
                        sprintf( '/%s/%s/%d', 'llms/v1', 'memberships', $enrollment->post_id )
                    ),
                );
                break;
        endswitch;

        /**
         * Filters the enrollment's links.
         *
         * @since 1.0.0-beta.10
         *
         * @param array    $links      Links for the given enrollment.
         * @param stdClass $enrollment Enrollment object.
         */
        return apply_filters( 'llms_rest_enrollment_links', $links, $enrollment );

    }

    /**
     * Handles the enrollment status update.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.10 Added the `trigger` paramater.
     * @since 1.0.0-beta.26 Fixed passing a 3rd parameter to `LLMS_Student::enroll()` method.
     *
     * @param LLMS_Student $student Student.
     * @param integer      $post_id The post id.
     * @param string       $status  Status.
     * @param string       $trigger The enrollment trigger.
     * @return boolean
     */
    protected function handle_status_update( $student, $post_id, $status, $trigger ) {

        // Status.
        switch ( $status ) :
            case 'enrolled':
                // The default trigger for the `LLMS_Student::enroll()` method is 'unspecified'.
                $trigger = $trigger && 'any' !== $trigger ? $trigger : 'unspecified';
                $updated = $student->enroll( $post_id, $trigger );
                break;
            default:
                $updated = $student->unenroll( $post_id, $trigger, $status );
        endswitch;

        return $updated;

    }


    /**
     * Handles the enrollment creation date.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.4 Fixed call to undefined function `llms_bad_request_error()`, must be `llms_rest_bad_request_error()`.
     *
     * @param integer $student_id Student id.
     * @param integer $post_id    The post id.
     * @param string  $date       Creation date.
     * @return boolean
     */
    protected function handle_creation_date_update( $student_id, $post_id, $date ) {

        $date_created = rest_parse_date( $date );

        if ( ! $date_created ) {
            return llms_rest_bad_request_error();
        }

        $date_created = date_i18n( 'Y-m-d H:i:s', $date_created );

        global $wpdb;

        $inner_query = $wpdb->prepare(
            "
            SELECT upm2.meta_id
            FROM ( SELECT * FROM {$wpdb->prefix}lifterlms_user_postmeta ) AS upm2
            WHERE upm2.meta_key = '_start_date' AND upm2.user_id = %d AND upm2.post_id = %d
            ORDER BY upm2.updated_date DESC
            LIMIT 1
            ",
            $student_id,
            $post_id
        );

        $result = $wpdb->query(
            $wpdb->prepare(
                // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- it is prepared.
                "UPDATE {$wpdb->prefix}lifterlms_user_postmeta SET updated_date = %s WHERE meta_id = ({$inner_query});",
                $date_created
            )
        ); // no-cache ok.

        return $result;
    }

    /**
     * Checks if an enrollment can be edited.
     *
     * @since 1.0.0-beta.1
     *
     * @return bool Whether the enrollment can be created
     */
    protected function check_create_permission() {
        return current_user_can( 'enroll' );
    }

    /**
     * Checks if an enrollment can be updated
     *
     * @since 1.0.0-beta.1
     *
     * @return bool Whether the enrollment can be edited.
     */
    protected function check_update_permission() {
        return current_user_can( 'enroll' ) && current_user_can( 'unenroll' );
    }

    /**
     * Checks if an enrollment can be deleted
     *
     * @since 1.0.0-beta.1
     *
     * @return bool Whether the enrollment can be deleted.
     */
    protected function check_delete_permission() {
        return current_user_can( 'unenroll' );
    }

    /**
     * Checks if an enrollment can be read.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.4 The single enrollment can be read only by who can view the enrollment's student.
     *
     * @param mixed $enrollment The enrollment object.
     * @return bool Whether the enrollment can be read.
     */
    protected function check_read_permission( $enrollment ) {

        /**
         * As of now, enrollments of password protected courses cannot be read
         */
        if ( isset( $enrollment->post_id ) && post_password_required( $enrollment->post_id ) ) {
            return false;
        }

        if ( get_current_user_id() === (int) $enrollment->student_id ) {
            return true;
        }

        return current_user_can( 'view_students', $enrollment->student_id );

    }

}