gocodebox/lifterlms-rest

View on GitHub
includes/abstracts/class-llms-rest-posts-controller.php

Summary

Maintainability
F
1 wk
Test Coverage
B
85%
<?php
/**
 * REST LLMS Posts Controller Class
 *
 * @package LifterLMS_REST/Abstracts
 *
 * @since 1.0.0-beta.1
 * @version 1.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_REST_Posts_Controller
 *
 * @since 1.0.0-beta.1
 * @since 1.0.0-beta.2 Filter taxonomies by `public` property instead of `show_in_rest`.
 * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`.
 * @since 1.0.0-beta.7 Added: `check_read_object_permissions()`, `get_objects_from_query()`, `get_objects_query()`, `get_pagination_data_from_query()`, `prepare_collection_items_for_response()` methods overrides.
 *                     `get_items()` method removed, now abstracted in LLMS_REST_Controller.
 *                     `prepare_objects_query()` renamed to `prepare_collection_query_args()`.
 *                     On `update_item`, don't execute `$object->set_bulk()` when there's no data to update.
 *                     Fix wp:featured_media link, we don't expose any embeddable field.
 *                     Also `self` and `collection` links prepared in the parent class.
 *                     Added `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks:
 *                     fired after inserting/updating an llms post into the database.
 * @since 1.0.0-beta.8 Return links to those taxonomies which have an accessible rest route.
 *                     Initialize `$prepared_item` array before adding values to it.
 * @since 1.0.0-beta.9 Implemented a generic way to create and get an llms post object instance given a `post_type`.
 *                     In `get_objects_from_query()` avoid performing an additional query, just return the already retrieved posts.
 *                     Removed `"llms_rest_{$this->post_type}_filters_removed_for_response"` filter hooks,
 *                     `"llms_rest_{$this->post_type}_filters_removed_for_response"` added.
 * @since 1.0.0-beta.11 Fixed `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks fourth param:
 *                     must be false when updating.
 * @since 1.0.0-beta.12 Moved parameters to query args mapping from `$this->prepare_collection_params()` to `$this->map_params_to_query_args()`.
 * @since 1.0.0-beta.14 Update `prepare_links()` to accept a second parameter, `WP_REST_Request`.
 * @since 1.0.0-beta.21 Enable search.
 */
abstract class LLMS_REST_Posts_Controller extends LLMS_REST_Controller {

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

    /**
     * Route base.
     *
     * @var string
     */
    protected $collection_route_base_for_pagination;

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

    /**
     * Whether search is allowed
     *
     * @var boolean
     */
    protected $is_searchable = true;

    /**
     * LLMS post class name.
     *
     * @since 1.0.0-beta.9
     *
     * @var string
     */
    protected $llms_post_class;

    /**
     * Constructor.
     *
     * @since 1.0.0-beta.27
     *
     * @return void
     */
    public function __construct() {
        $this->meta = new WP_REST_Post_Meta_Fields( $this->post_type );
    }

    /**
     * Retrieves an array of arguments for the delete endpoint.
     *
     * @since 1.0.0-beta.1
     *
     * @return array Delete endpoint arguments.
     */
    public function get_delete_item_args() {

        return array(
            'force' => array(
                'description' => __( 'Bypass the trash and force course deletion.', 'lifterlms' ),
                'type'        => 'boolean',
                'default'     => false,
            ),
        );

    }

    /**
     * Retrieves the query params for retrieving a single resource.
     *
     * @since 1.0.0-beta.1
     *
     * @return array
     */
    public function get_get_item_params() {

        $params = parent::get_get_item_params();
        $schema = $this->get_item_schema();

        if ( isset( $schema['properties']['password'] ) ) {
            $params['password'] = array(
                'description' => __( 'Post password. Required if the post is password protected.', 'lifterlms' ),
                'type'        => 'string',
            );
        }

        return $params;

    }

    /**
     * Determine if the current user can view the object.
     *
     * @since 1.0.0-beta.7
     *
     * @param object $object Object.
     * @return bool
     */
    protected function check_read_object_permissions( $object ) {
        return $this->check_read_permission( $object );
    }

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

        // Everybody can list llms posts (in read mode).
        if ( 'edit' === $request['context'] && ! $this->check_update_permission() ) {
            return llms_rest_authorization_required_error();
        }

        return true;

    }

    /**
     * Retrieve pagination information from an objects query.
     *
     * @since 1.0.0-beta.7
     *
     * @param WP_Query        $query    Objects query result returned by {@see LLMS_REST_Posts_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_posts;
        $current_page  = isset( $prepared['paged'] ) ? (int) $prepared['paged'] : 1;
        $total_pages   = (int) ceil( $total_results / (int) $query->get( 'posts_per_page' ) );

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

    }

    /**
     * Check if a given request has access to create an item.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.18 Use plural post type name.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function create_item_permissions_check( $request ) {

        $post_type_object = get_post_type_object( $this->post_type );
        $post_type_name   = $post_type_object->labels->name;

        if ( ! empty( $request['id'] ) ) {
            // Translators: %s = The post type name.
            return llms_rest_bad_request_error( sprintf( __( 'Cannot create existing %s.', 'lifterlms' ), $post_type_name ) );
        }

        if ( ! $this->check_create_permission() ) {
            // Translators: %s = The post type name.
            return llms_rest_authorization_required_error( sprintf( __( 'Sorry, you are not allowed to create %s as this user.', 'lifterlms' ), $post_type_name ) );
        }

        if ( ! $this->check_assign_terms_permission( $request ) ) {
            return llms_rest_authorization_required_error( __( 'Sorry, you are not allowed to assign the provided terms.', 'lifterlms' ) );
        }

        return true;
    }


    /**
     * Creates a single LLMS post.
     *
     * Extending classes can add additional object fields by overriding the method `update_additional_object_fields()`.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.7 Added `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks:
     *                     fired after inserting/updating an llms post into the database.
     * @since 1.0.0-beta.25 Allow updating meta with the same value as the stored one.
     * @since 1.0.0-beta.27 Handle custom meta registered via `register_meta()` and 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 ) {

        $schema = $this->get_item_schema();

        $prepared_item = $this->prepare_item_for_database( $request );
        if ( is_wp_error( $prepared_item ) ) {
            return $prepared_item;
        }

        $prepared_item = array_diff_key( $prepared_item, $this->get_additional_fields() );
        $object        = $this->create_llms_post( $prepared_item );
        if ( is_wp_error( $object ) ) {

            if ( 'db_insert_error' === $object->get_error_code() ) {
                $object->add_data( array( 'status' => 500 ) );
            } else {
                $object->add_data( array( 'status' => 400 ) );
            }

            return $object;
        }

        /**
         * Fires after a single llms post is created or updated via the REST API.
         *
         * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
         *
         * @since 1.0.0-beta.7
         *
         * @param LLMS_Post       $object   Inserted or updated llms object.
         * @param WP_REST_Request $request  Request object.
         * @param array           $schema   The item schema.
         * @param bool            $creating True when creating a post, false when updating.
         */
        do_action( "llms_rest_insert_{$this->post_type}", $object, $request, $schema, true );

        // Set all the other properties.
        // TODO: maybe we want to filter the post properties that have already been inserted before.
        $set_bulk_result = $object->set_bulk( $prepared_item, true, true );
        if ( is_wp_error( $set_bulk_result ) ) {

            if ( 'db_update_error' === $set_bulk_result->get_error_code() ) {
                $set_bulk_result->add_data( array( 'status' => 500 ) );
            } else {
                $set_bulk_result->add_data( array( 'status' => 400 ) );
            }

            return $set_bulk_result;
        }

        $object_id = $object->get( 'id' );

        $additional_fields = $this->update_additional_object_fields( $object, $request, $schema, $prepared_item );
        if ( is_wp_error( $additional_fields ) ) {
            return $additional_fields;
        }

        if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
            $this->handle_featured_media( $request['featured_media'], $object_id );
        }

        $terms_update = $this->handle_terms( $object_id, $request );
        if ( is_wp_error( $terms_update ) ) {
            return $terms_update;
        }

        $meta_update = $this->update_meta( $object, $request, $schema );
        if ( is_wp_error( $meta_update ) ) {
            return $meta_update;
        }

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

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

        /**
         * Fires after a single llms post is completely created or updated via the REST API.
         *
         * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
         *
         * @since 1.0.0-beta.7
         *
         * @param LLMS_Post       $object   Inserted or updated llms object.
         * @param WP_REST_Request $request  Request object.
         * @param array           $schema   The item schema.
         * @param bool            $creating True when creating a post, false when updating.
         */
        do_action( "llms_rest_after_insert_{$this->post_type}", $object, $request, $schema, true );

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

        $response->set_status( 201 );

        $response->header( 'Location', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $object_id ) ) );

        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 ) {

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

        if ( 'edit' === $request['context'] && ! $this->check_update_permission( $object ) ) {
            return llms_rest_authorization_required_error();
        }

        if ( ! empty( $request['password'] ) ) {
            // Check post password, and return error if invalid.
            if ( ! hash_equals( $object->get( 'password' ), $request['password'] ) ) {
                return llms_rest_authorization_required_error( __( 'Incorrect password.', 'lifterlms' ) );
            }
        }

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

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

        return true;
    }

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

        $query_params = parent::get_collection_params();
        $schema       = $this->get_item_schema();

        if ( isset( $schema['properties']['status'] ) ) {
            $query_params['status'] = array(
                'default'           => 'publish',
                'description'       => __( 'Limit result set to posts assigned one or more statuses.', 'lifterlms' ),
                'type'              => 'array',
                'items'             => array(
                    'enum' => array_merge(
                        array_keys(
                            get_post_stati()
                        ),
                        array(
                            'any',
                        )
                    ),
                    'type' => 'string',
                ),
                'sanitize_callback' => array( $this, 'sanitize_post_statuses' ),
            );
        }

        return $query_params;

    }

    /**
     * Format query arguments to retrieve a collection of objects.
     *
     * @since 1.0.0-beta.7
     * @since 1.0.0-beta.12 Moved parameters to query args mapping into a different method.
     * @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;
        }

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

        $query_args = $this->prepare_items_query( $prepared, $request );

        return $query_args;

    }

    /**
     * Map schema to query arguments to retrieve a collection of objects.
     *
     * @since 1.0.0-beta.12
     * @since 1.0.0-beta.19 Map 'status' collection param to to 'post_status' query arg.
     *
     * @param array           $prepared   Array of collection arguments.
     * @param array           $registered Registered collection params.
     * @param WP_REST_Request $request    Full details about the request.
     * @return array|WP_Error
     */
    protected function map_params_to_query_args( $prepared, $registered, $request ) {

        $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(
            'order'   => 'order',
            'orderby' => 'orderby',
            'page'    => 'paged',
            'exclude' => 'post__not_in',
            'include' => 'post__in',
            'search'  => 's',
            '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 ];
            }
        }

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

        return $args;
    }

    /**
     * Check if a given request has access to update an item.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.18 Use plural post type name.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function update_item_permissions_check( $request ) {

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

        $post_type_object = get_post_type_object( $this->post_type );
        $post_type_name   = $post_type_object->labels->name;

        if ( ! $this->check_update_permission( $object ) ) {
            // Translators: %s = The post type name.
            return llms_rest_authorization_required_error( sprintf( __( 'Sorry, you are not allowed to update %s as this user.', 'lifterlms' ), $post_type_name ) );
        }

        if ( ! $this->check_assign_terms_permission( $request ) ) {
            return llms_rest_authorization_required_error( __( 'Sorry, you are not allowed to assign the provided terms.', 'lifterlms' ) );
        }

        return true;
    }

    /**
     * Updates a single llms post.
     *
     * Extending classes can add additional object fields by overriding the method `update_additional_object_fields()`.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.7 Don't execute `$object->set_bulk()` when there's no data to update:
     *                     this fixes an issue when updating only properties which are not handled in `prepare_item_for_database()`.
     *                     Added `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks:
     *                     fired after inserting/updating an llms post into the database.
     * @since 1.0.0-beta.11 Fixed `"llms_rest_insert_{$this->post_type}"` and `"llms_rest_insert_{$this->post_type}"` action hooks fourth param:
     *                      must be false when updating.
     * @since 1.0.0-beta.25 Allow updating meta with the same value as the stored one.
     * @since 1.0.0-beta.27 Handle custom meta registered via `register_meta()` and 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 update_item( $request ) {

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

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

        if ( is_wp_error( $prepared_item ) ) {
            return $prepared_item;
        }
        $prepared_item = array_diff_key( $prepared_item, $this->get_additional_fields() );
        $update_result = empty( array_diff_key( $prepared_item, array_flip( array( 'id' ) ) ) ) ? false : $object->set_bulk( $prepared_item, true, true );
        if ( is_wp_error( $update_result ) ) {

            if ( 'db_update_error' === $update_result->get_error_code() ) {
                $update_result->add_data( array( 'status' => 500 ) );
            } else {
                $update_result->add_data( array( 'status' => 400 ) );
            }

            return $update_result;
        }

        /**
         * Fires after a single llms post is created or updated via the REST API.
         *
         * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
         *
         * @since 1.0.0-beta.7
         *
         * @param LLMS_Post       $object   Inserted or updated llms object.
         * @param WP_REST_Request $request  Request object.
         * @param array           $schema   The item schema.
         * @param bool            $creating True when creating a post, false when updating.
         */
        do_action( "llms_rest_insert_{$this->post_type}", $object, $request, $schema, false );

        $additional_fields = $this->update_additional_object_fields( $object, $request, $schema, $prepared_item, false );
        if ( is_wp_error( $additional_fields ) ) {
            return $additional_fields;
        }

        $object_id = $object->get( 'id' );

        if ( ! empty( $schema['properties']['featured_media'] ) && isset( $request['featured_media'] ) ) {
            $this->handle_featured_media( $request['featured_media'], $object_id );
        }

        $terms_update = $this->handle_terms( $object_id, $request );
        if ( is_wp_error( $terms_update ) ) {
            return $terms_update;
        }

        $meta_update = $this->update_meta( $object, $request, $schema );
        if ( is_wp_error( $meta_update ) ) {
            return $meta_update;
        }

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

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

        /**
         * Fires after a single llms post is completely created or updated via the REST API.
         *
         * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
         *
         * @since 1.0.0-beta.7
         *
         * @param LLMS_Post       $object   Inserted or updated llms object.
         * @param WP_REST_Request $request  Request object.
         * @param array           $schema   The item schema.
         * @param bool            $creating True when creating a post, false when updating.
         */
        do_action( "llms_rest_after_insert_{$this->post_type}", $object, $request, $schema, false );

        return $this->prepare_item_for_response( $object, $request );

    }

    /**
     * Updates a single llms post.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.7 return description updated.
     *
     * @param LLMS_Post_Model $object        LMMS_Post_Model instance.
     * @param array           $prepared_item Array.
     * @param WP_REST_Request $request       Full details about the request.
     * @param array           $schema        The item schema.
     * @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( $object, $prepared_item, $request, $schema ) {
        return true;
    }

    /**
     * Check if a given request has access to delete an item.
     *
     * @since 1.0.0-beta.1
     * @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 bool|WP_Error
     */
    public function delete_item_permissions_check( $request ) {

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

            return $object;
        }

        if ( ! $this->check_delete_permission( $object ) ) {
            return llms_rest_authorization_required_error(
                sprintf(
                    // Translators: %s = The post type name.
                    __( 'Sorry, you are not allowed to delete %s 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
     *
     * @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 ) {

        $object   = $this->get_object( (int) $request['id'] );
        $response = new WP_REST_Response();
        $response->set_status( 204 );

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

            return $object;
        }

        $post_type_object = get_post_type_object( $this->post_type );
        $post_type_name   = $post_type_object->labels->singular_name;

        $id    = $object->get( 'id' );
        $force = $this->is_delete_forced( $request );

        // If we're forcing, then delete permanently.
        if ( $force ) {
            $result = wp_delete_post( $id, true );
        } else {

            $supports_trash = $this->is_trash_supported();

            // If we don't support trashing for this type, error out.
            if ( ! $supports_trash ) {
                return new WP_Error(
                    'llms_rest_trash_not_supported',
                    /* translators: %1$s: post type name, %2$s: force=true */
                    sprintf( __( 'The %1$s does not support trashing. Set \'%2$s\' to delete.', 'lifterlms' ), $post_type_name, 'force=true' ),
                    array( 'status' => 501 )
                );
            }

            // Otherwise, only trash if we haven't already.
            if ( 'trash' !== $object->get( 'status' ) ) {
                // (Note that internally this falls through to `wp_delete_post` if
                // the trash is disabled.)
                $result = wp_trash_post( $id );
            } else {
                $result = true;
            }

            $request->set_param( 'context', 'edit' );
            $object   = $this->get_object( $id );
            $response = $this->prepare_item_for_response( $object, $request );

        }

        if ( ! $result ) {
            return new WP_Error(
                'llms_rest_cannot_delete',
                /* translators: %s: post type name */
                sprintf( __( 'The %s cannot be deleted.', 'lifterlms' ), $post_type_name ),
                array( 'status' => 500 )
            );
        }

        return $response;

    }

    /**
     * Whether the delete should be forced.
     *
     * @since 1.0.0-beta.1
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return bool True if the delete should be forced, false otherwise.
     */
    protected function is_delete_forced( $request ) {
        return isset( $request['force'] ) && (bool) $request['force'];
    }

    /**
     * Whether the trash is supported.
     *
     * @since 1.0.0-beta.1
     *
     * @return bool True if the trash is supported, false otherwise.
     */
    protected function is_trash_supported() {
        return ( EMPTY_TRASH_DAYS > 0 );
    }


    /**
     * Retrieve a query object based on arguments from a `get_items()` (collection) request.
     *
     * @since 1.0.0-beta.7
     *
     * @param  array           $prepared Array of collection arguments.
     * @param  WP_REST_Request $request  Full details about the request.
     * @return WP_Query
     */
    protected function get_objects_query( $prepared, $request ) {

        return new WP_Query( $prepared );

    }

    /**
     * Retrieve an array of objects from the result of `$this->get_objects_query()`.
     *
     * @since 1.0.0-beta.7
     * @since 1.0.0-beta.9 Avoid performing an additional query, just return the already retrieved posts.
     *
     * @param WP_Query $query WP_Query query result.
     * @return WP_Post[]
     */
    protected function get_objects_from_query( $query ) {

        return $query->posts;

    }

    /**
     * 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();

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

        $items = parent::prepare_collection_items_for_response( $objects, $request );

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

        return $items;

    }

    /**
     * Prepare a single object output for response.
     *
     * @since 1.0.0-beta.1
     *
     * @param LLMS_Post_Model $object  object object.
     * @param WP_REST_Request $request Full details about the request.
     * @return array
     */
    protected function prepare_object_for_response( $object, $request ) {

        $object_id         = $object->get( 'id' );
        $password_required = post_password_required( $object_id );
        $password          = $object->get( 'password' );

        $data = array(
            'id'               => $object->get( 'id' ),
            'date_created'     => $object->get_date( 'date', 'Y-m-d H:i:s' ),
            'date_created_gmt' => $object->get_date( 'date_gmt', 'Y-m-d H:i:s' ),
            'date_updated'     => $object->get_date( 'modified', 'Y-m-d H:i:s' ),
            'date_updated_gmt' => $object->get_date( 'modified_gmt', 'Y-m-d H:i:s' ),
            'menu_order'       => $object->get( 'menu_order' ),
            'title'            => array(
                'raw'      => $object->get( 'title', true ),
                'rendered' => $object->get( 'title' ),
            ),
            'password'         => $password,
            'slug'             => $object->get( 'name' ),
            'post_type'        => $this->post_type,
            'permalink'        => get_permalink( $object_id ),
            'status'           => $object->get( 'status' ),
            'featured_media'   => (int) get_post_thumbnail_id( $object_id ),
            'comment_status'   => $object->get( 'comment_status' ),
            'ping_status'      => $object->get( 'ping_status' ),
            'content'          => array(
                'raw'       => $object->get( 'content', true ),
                'rendered'  => $password_required ? '' : apply_filters( 'the_content', $object->get( 'content', true ) ),
                'protected' => (bool) $password,
            ),
            'excerpt'          => array(
                'raw'       => $object->get( 'excerpt', true ),
                'rendered'  => $password_required ? '' : apply_filters( 'the_excerpt', $object->get( 'excerpt' ) ),
                'protected' => (bool) $password,
            ),
        );

        return $data;

    }

    /**
     * Prepares data of a single object for response.
     *
     * @since 1.0.0-beta.27
     *
     * @param obj             $object  Raw object from database.
     * @param WP_REST_Request $request Request object.
     * @return array
     */
    protected function prepare_object_data_for_response( $object, $request ) {

        // Need to set the global $post because of references to the global $post when e.g. filtering the content, or processing blocks/shortcodes.
        global $post;
        $temp = $post;
        $post = $object->get( 'post' ); // phpcs:ignore
        setup_postdata( $post );

        $removed_filters_for_response = $this->maybe_remove_filters_for_response( $object );

        $has_password_filter = false;

        if ( $this->can_access_password_content( $object, $request ) ) {
            // Allow access to the post, permissions already checked before.
            add_filter( 'post_password_required', '__return_false' );
            $has_password_filter = true;
        }

        $data = parent::prepare_object_data_for_response( $object, $request );

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

        if ( $has_password_filter ) {
            // Reset filter.
            remove_filter( 'post_password_required', '__return_false' );
        }

        $this->maybe_add_removed_filters_for_response( $removed_filters_for_response );
        $post = $temp; // phpcs:ignore
        wp_reset_postdata();

        return $data;

    }

    /**
     * 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;
        }

        $query_args = $this->prepare_items_query_orderby_mappings( $query_args, $request );

        // Turn exclude and include params into proper arrays.
        foreach ( array( 'post__in', 'post__not_in' ) as $arg ) {
            if ( isset( $query_args[ $arg ] ) && ! is_array( $query_args[ $arg ] ) ) {
                $query_args[ $arg ] = array_map( 'absint', explode( ',', $query_args[ $arg ] ) );
            }
        }

        return $query_args;

    }

    /**
     * Map to proper WP_Query orderby param.
     *
     * @since 1.0.0-beta.1
     *
     * @param array           $query_args WP_Query arguments.
     * @param WP_REST_Request $request    Full details about the request.
     * @return array Query arguments.
     */
    protected function prepare_items_query_orderby_mappings( $query_args, $request ) {

        // Map to proper WP_Query orderby param.
        if ( isset( $query_args['orderby'] ) && isset( $request['orderby'] ) ) {
            $orderby_mappings = array(
                'id'           => 'ID',
                'title'        => 'title',
                'data_created' => 'post_date',
                'date_updated' => 'post_modified',
            );

            if ( isset( $orderby_mappings[ $request['orderby'] ] ) ) {
                $query_args['orderby'] = $orderby_mappings[ $request['orderby'] ];
            }
        }

        return $query_args;

    }

    /**
     * Prepares a single post for create or update.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.8 Initialize `$prepared_item` array before adding values to it.
     *
     * @param WP_REST_Request $request  Request object.
     * @return array|WP_Error Array of llms post args or WP_Error.
     */
    protected function prepare_item_for_database( $request ) {

        $prepared_item = array();

        // LLMS Post ID.
        if ( isset( $request['id'] ) ) {
            $existing_object = $this->get_object( absint( $request['id'] ) );
            if ( is_wp_error( $existing_object ) ) {
                return $existing_object;
            }

            $prepared_item['id'] = absint( $request['id'] );
        }

        $schema = $this->get_item_schema();

        // LLMS Post title.
        if ( ! empty( $schema['properties']['title'] ) && isset( $request['title'] ) ) {
            if ( is_string( $request['title'] ) ) {
                $prepared_item['post_title'] = $request['title'];
            } elseif ( ! empty( $request['title']['raw'] ) ) {
                $prepared_item['post_title'] = $request['title']['raw'];
            }
        }

        // LLMS Post content.
        if ( ! empty( $schema['properties']['content'] ) && isset( $request['content'] ) ) {
            if ( is_string( $request['content'] ) ) {
                $prepared_item['post_content'] = $request['content'];
            } elseif ( isset( $request['content']['raw'] ) ) {
                $prepared_item['post_content'] = $request['content']['raw'];
            }
        }

        // LLMS Post excerpt.
        if ( ! empty( $schema['properties']['excerpt'] ) && isset( $request['excerpt'] ) ) {
            if ( is_string( $request['excerpt'] ) ) {
                $prepared_item['post_excerpt'] = $request['excerpt'];
            } elseif ( isset( $request['excerpt']['raw'] ) ) {
                $prepared_item['post_excerpt'] = $request['excerpt']['raw'];
            }
        }

        // LLMS Post status.
        if ( ! empty( $schema['properties']['status'] ) && isset( $request['status'] ) ) {
            $status = $this->handle_status_param( $request['status'] );
            if ( is_wp_error( $status ) ) {
                return $status;
            }

            $prepared_item['post_status'] = $status;
        }

        // LLMS Post date.
        if ( ! empty( $schema['properties']['date_created'] ) && ! empty( $request['date_created'] ) ) {
            $date_data = rest_get_date_with_gmt( $request['date_created'] );

            if ( ! empty( $date_data ) ) {
                list( $prepared_item['post_date'], $prepared_item['post_date_gmt'] ) = $date_data;
                $prepared_item['edit_date'] = true;
            }
        } elseif ( ! empty( $schema['properties']['date_gmt'] ) && ! empty( $request['date_gmt'] ) ) {
            $date_data = rest_get_date_with_gmt( $request['date_created_gmt'], true );

            if ( ! empty( $date_data ) ) {
                list( $prepared_item['post_date'], $prepared_item['post_date_gmt'] ) = $date_data;
                $prepared_item['edit_date'] = true;
            }
        }

        // LLMS Post slug.
        if ( ! empty( $schema['properties']['slug'] ) && isset( $request['slug'] ) ) {
            $prepared_item['post_name'] = $request['slug'];
        }

        // LLMS Post password.
        if ( ! empty( $schema['properties']['password'] ) && isset( $request['password'] ) ) {
            $prepared_item['post_password'] = $request['password'];
        }

        // LLMS Post Menu order.
        if ( ! empty( $schema['properties']['menu_order'] ) && isset( $request['menu_order'] ) ) {
            $prepared_item['menu_order'] = (int) $request['menu_order'];
        }

        // LLMS Post Comment status.
        if ( ! empty( $schema['properties']['comment_status'] ) && ! empty( $request['comment_status'] ) ) {
            $prepared_item['comment_status'] = $request['comment_status'];
        }

        // LLMS Post Ping status.
        if ( ! empty( $schema['properties']['ping_status'] ) && ! empty( $request['ping_status'] ) ) {
            $prepared_item['ping_status'] = $request['ping_status'];
        }

        return $prepared_item;

    }

    /**
     * Get the LLMS Posts'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'      => $this->post_type,
            'type'       => 'object',
            'properties' => array(
                'id'               => array(
                    'description' => __( 'Unique Identifier. The WordPress Post ID.', '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_created_gmt' => array(
                    'description' => __( 'Creation date (in GMT). 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,
                ),
                'date_updated_gmt' => array(
                    'description' => __( 'Date last modified (in GMT). Format: Y-m-d H:i:s', 'lifterlms' ),
                    'type'        => 'string',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'menu_order'       => array(
                    'description' => __( 'Creation date (in GMT). Format: Y-m-d H:i:s', 'lifterlms' ),
                    'type'        => 'integer',
                    'default'     => 0,
                    'context'     => array( 'view', 'edit' ),
                    'arg_options' => array(
                        'sanitize_callback' => 'absint',
                    ),
                ),
                'title'            => array(
                    'description' => __( 'Post title.', '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().
                    ),
                    'required'    => true,
                    'properties'  => array(
                        'raw'      => array(
                            'description' => __( 'Raw title. Useful when displaying title in the WP Block Editor. Only returned in edit context.', 'lifterlms' ),
                            'type'        => 'string',
                            'context'     => array( 'edit' ),
                        ),
                        'rendered' => array(
                            'description' => __( 'Rendered title.', 'lifterlms' ),
                            'type'        => 'string',
                            'context'     => array( 'view', 'edit' ),
                            'readonly'    => true,
                        ),
                    ),
                ),
                'content'          => array(
                    'type'        => 'object',
                    'description' => __( 'The HTML content of the post.', 'lifterlms' ),
                    '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().
                    ),
                    'required'    => true,
                    'properties'  => array(
                        'rendered'  => array(
                            'description' => __( 'Rendered HTML content.', 'lifterlms' ),
                            'type'        => 'string',
                            'context'     => array( 'view', 'edit' ),
                            'readonly'    => true,
                        ),
                        'raw'       => array(
                            'description' => __( 'Raw HTML content. Useful when displaying title in the WP Block Editor. Only returned in edit context.', 'lifterlms' ),
                            'type'        => 'string',
                            'context'     => array( 'edit' ),
                        ),
                        'protected' => array(
                            'description' => __( 'Whether the content is protected with a password.', 'lifterlms' ),
                            'type'        => 'boolean',
                            'context'     => array( 'view', 'edit' ),
                            'readonly'    => true,
                        ),
                    ),
                ),
                'excerpt'          => array(
                    'type'        => 'object',
                    'description' => __( 'The HTML excerpt of the post.', 'lifterlms' ),
                    '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(
                        'rendered'  => array(
                            'description' => __( 'Rendered HTML excerpt.', 'lifterlms' ),
                            'type'        => 'string',
                            'context'     => array( 'view', 'edit' ),
                            'readonly'    => true,
                        ),
                        'raw'       => array(
                            'description' => __( 'Raw HTML excerpt. Useful when displaying title in the WP Block Editor. Only returned in edit context.', 'lifterlms' ),
                            'type'        => 'string',
                            'context'     => array( 'edit' ),
                        ),
                        'protected' => array(
                            'description' => __( 'Whether the excerpt is protected with a password.', 'lifterlms' ),
                            'type'        => 'boolean',
                            'context'     => array( 'view', 'edit' ),
                            'readonly'    => true,
                        ),
                    ),
                ),
                'permalink'        => array(
                    'description' => __( 'Post URL.', 'lifterlms' ),
                    'type'        => 'string',
                    'format'      => 'uri',
                    'context'     => array( 'view', 'edit' ),
                    'readonly'    => true,
                ),
                'slug'             => array(
                    'description' => __( 'Post URL slug.', 'lifterlms' ),
                    'type'        => 'string',
                    'context'     => array( 'view', 'edit' ),
                    'arg_options' => array(
                        'sanitize_callback' => array( $this, 'sanitize_slug' ),
                    ),
                ),
                'post_type'        => array(
                    'description' => __( 'LifterLMS custom post type', 'lifterlms' ),
                    'type'        => 'string',
                    'readonly'    => true,
                    'context'     => array( 'view', 'edit' ),
                ),
                'status'           => array(
                    'description' => __( 'The publication status of the post.', 'lifterlms' ),
                    'type'        => 'string',
                    'default'     => 'publish',
                    'enum'        => array_keys(
                        get_post_stati(
                            array(
                                '_builtin' => true,
                                'internal' => false,
                            )
                        )
                    ),
                    'context'     => array( 'view', 'edit' ),
                ),
                'password'         => array(
                    'description' => __( 'Password used to protect access to the content.', 'lifterlms' ),
                    'type'        => 'string',
                    'context'     => array( 'edit' ),
                ),
                'featured_media'   => array(
                    'description' => __( 'Featured image ID.', 'lifterlms' ),
                    'type'        => 'integer',
                    'context'     => array( 'view', 'edit' ),
                ),
                'comment_status'   => array(
                    'description' => __( 'Post comment status. Default comment status dependent upon general WordPress post discussion settings.', 'lifterlms' ),
                    'type'        => 'string',
                    'default'     => 'open',
                    'enum'        => array( 'open', 'closed' ),
                    'context'     => array( 'view', 'edit' ),
                ),
                'ping_status'      => array(
                    'description' => __( 'Post ping status. Default ping status dependent upon general WordPress post discussion settings.', 'lifterlms' ),
                    'type'        => 'string',
                    'default'     => 'open',
                    'enum'        => array( 'open', 'closed' ),
                    'context'     => array( 'view', 'edit' ),
                ),
            ),
        );

        return $schema;

    }

    /**
     * Add custom fields registered via `register_meta`.
     *
     * @since 1.0.0-beta.27
     *
     * @param array $schema The resource item schema.
     * @return array
     */
    protected function add_meta_fields_schema( $schema ) {
        return post_type_supports( $this->post_type, 'custom-fields' ) ? parent::add_meta_fields_schema( $schema ) : $schema;
    }

    /**
     * Get object.
     *
     * @since 1.0.0-beta.9
     *
     * @param int $id Object ID.
     * @return LLMS_Course|WP_Error
     */
    protected function get_object( $id ) {

        $class = $this->llms_post_class_from_post_type();

        if ( ! $class ) {
            return new WP_Error(
                'llms_rest_cannot_get_object',
                /* translators: %s: post type */
                sprintf( __( 'The %s cannot be retrieved.', 'lifterlms' ), $this->post_type ),
                array( 'status' => 500 )
            );
        }

        $object = llms_get_post( $id );
        return $object && is_a( $object, $class ) ? $object : llms_rest_not_found_error();
    }

    /**
     * Create an LLMS_Post_Model
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.9 Implement generic llms post creation.
     *
     * @param array $object_args Object args.
     * @return LLMS_Post_Model|WP_Error
     */
    protected function create_llms_post( $object_args ) {

        $class = $this->llms_post_class_from_post_type();

        if ( ! $class ) {
            return new WP_Error(
                'llms_rest_cannot_create_object',
                /* translators: %s: post type */
                sprintf( __( 'The %s cannot be created.', 'lifterlms' ), $this->post_type ),
                array( 'status' => 500 )
            );
        }

        $object = new $class( 'new', $object_args );
        return $object && is_a( $object, $class ) ? $object : llms_rest_not_found_error();
    }

    /**
     * Prepare links for the request.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.2 Filter taxonomies by `public` property instead of `show_in_rest`.
     * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`.
     * @since 1.0.0-beta.7 `self` and `collection` links prepared in the parent class.
     *                     Fix wp:featured_media link, we don't expose any embeddable field.
     * @since 1.0.0-beta.8 Return links to those taxonomies which have an accessible rest route.
     * @since 1.0.0-beta.14 Added $request parameter.
     *
     * @param LLMS_Post_Model $object  Object data.
     * @param WP_REST_Request $request Request object.
     * @return array Links for the given object.
     */
    protected function prepare_links( $object, $request ) {

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

        $object_id = $object->get( 'id' );

        // Content.
        $links['content'] = array(
            'href' => rest_url( sprintf( '/%s/%s/%d/%s', $this->namespace, $this->rest_base, $object_id, 'content' ) ),
        );

        // If we have a featured media, add that.
        $featured_media = get_post_thumbnail_id( $object_id );
        if ( $featured_media ) {
            $image_url = rest_url( 'wp/v2/media/' . $featured_media );

            $links['https://api.w.org/featuredmedia'] = array(
                'href' => $image_url,
            );
        }

        $taxonomies = get_object_taxonomies( $this->post_type );

        if ( ! empty( $taxonomies ) ) {
            $links['https://api.w.org/term'] = array();

            foreach ( $taxonomies as $tax ) {
                $taxonomy_obj = get_taxonomy( $tax );

                // Skip taxonomies that are not set to be shown in REST and LLMS REST.
                if ( empty( $taxonomy_obj->show_in_rest ) || empty( $taxonomy_obj->show_in_llms_rest ) ) {
                    continue;
                }

                $tax_base = ! empty( $taxonomy_obj->rest_base ) ? $taxonomy_obj->rest_base : $tax;

                $terms_url = add_query_arg(
                    'post',
                    $object_id,
                    rest_url( 'wp/v2/' . $tax_base )
                );

                $links['https://api.w.org/term'][] = array(
                    'href'     => $terms_url,
                    'taxonomy' => $tax,
                );
            }
        }

        return $links;

    }

    /**
     * Re-add filters previously removed
     *
     * @since 1.0.0-beta.1
     *
     * @param LLMS_Post_Model $object Object.
     * @return array Array of filters removed for response.
     */
    protected function maybe_remove_filters_for_response( $object ) {

        $filters_to_be_removed = $this->get_filters_to_be_removed_for_response( $object );
        $filters_removed       = array();

        // Need to remove some filters.
        foreach ( $filters_to_be_removed as $hook => $filters ) {
            foreach ( $filters as $filter_data ) {
                $has_filter = has_filter( $hook, $filter_data['callback'] );

                if ( false !== $has_filter && $filter_data['priority'] === $has_filter ) {
                    remove_filter( $hook, $filter_data['callback'], $filter_data['priority'] );
                    if ( ! isset( $filters_removed[ $hook ] ) ) {
                        $filters_removed[ $hook ] = array();
                    }
                    $filters_removed[ $hook ][] = $filter_data;

                }
            }
        }

        return $filters_removed;

    }

    /**
     * Re-add filters previously removed
     *
     * @since 1.0.0-beta.1
     *
     * @param array $filters_removed Array of filters removed to be re-added.
     * @return void
     */
    protected function maybe_add_removed_filters_for_response( $filters_removed ) {

        if ( ! empty( $filters_removed ) ) {
            foreach ( $filters_removed as $hook => $filters ) {
                foreach ( $filters as $filter_data ) {
                    add_filter(
                        $hook,
                        $filter_data['callback'],
                        $filter_data['priority'],
                        isset( $filter_data['accepted_args'] ) ? $filter_data['accepted_args'] : 1
                    );
                }
            }
        }
    }

    /**
     * Get action/filters to be removed before preparing the item for response.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.9 Removed `"llms_rest_{$this->post_type}_filters_removed_for_reponse"` filter hooks,
     *                     `"llms_rest_{$this->post_type}_filters_removed_for_response"` added.
     *
     * @param LLMS_Post_Model $object LLMS_Post_Model object.
     * @return array Array of action/filters to be removed for response.
     */
    protected function get_filters_to_be_removed_for_response( $object ) {

        /**
         * Modify the array of filters to be removed before building the response.
         *
         * The dynamic portion of the hook name, `$this->post_type`, refers to the post type slug.
         *
         * @since 1.0.0-beta.9
         *
         * @param array           $filters Array of filters to be removed.
         * @param LLMS_Post_Model $object  LLMS_Post_Model object.
         */
        return apply_filters( "llms_rest_{$this->post_type}_filters_removed_for_response", array(), $object );

    }

    /**
     * Determines validity and normalizes the given status parameter.
     * Heavily based on WP_REST_Posts_Controller::handle_status_param().
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.18 Use plural post type name.
     *
     * @param string $status Status.
     * @return string|WP_Error Status or WP_Error if lacking the proper permission.
     */
    protected function handle_status_param( $status ) {

        $post_type_object = get_post_type_object( $this->post_type );
        $post_type_name   = $post_type_object->labels->name;

        switch ( $status ) {
            case 'draft':
            case 'pending':
                break;
            case 'private':
                if ( ! current_user_can( $post_type_object->cap->publish_posts ) ) {
                    // Translators: %s = The post type name.
                    return llms_rest_authorization_required_error( sprintf( __( 'Sorry, you are not allowed to create private %s.', 'lifterlms' ), $post_type_name ) );
                }
                break;
            case 'publish':
            case 'future':
                if ( ! current_user_can( $post_type_object->cap->publish_posts ) ) {
                    // Translators: $s = The post type name.
                    return llms_rest_authorization_required_error( sprintf( __( 'Sorry, you are not allowed to publish %s.', 'lifterlms' ), $post_type_name ) );
                }
                break;
            default:
                if ( ! get_post_status_object( $status ) ) {
                    $status = 'draft';
                }
                break;
        }

        return $status;
    }

    /**
     * Determines the featured media based on a request param
     *
     * Heavily based on WP_REST_Posts_Controller::handle_featured_media().
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.18 Fixed call to undefined function `llms_bad_request_error()`, must be `llms_rest_bad_request_error()`.
     *
     * @param int $featured_media Featured Media ID.
     * @param int $object_id      LLMS object ID.
     * @return bool|WP_Error Whether the post thumbnail was successfully deleted, otherwise WP_Error.
     */
    protected function handle_featured_media( $featured_media, $object_id ) {

        $featured_media = (int) $featured_media;
        if ( $featured_media ) {
            $result = set_post_thumbnail( $object_id, $featured_media );
            if ( $result ) {
                return true;
            } else {
                return llms_rest_bad_request_error( __( 'Invalid featured media ID.', 'lifterlms' ) );
            }
        } else {
            return delete_post_thumbnail( $object_id );
        }

    }

    /**
     * Updates the post's terms from a REST request.
     *
     * Heavily based on WP_REST_Posts_Controller::handle_terms().
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.2 Filter taxonomies by `public` property instead of `show_in_rest`.
     * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`.
     *
     * @param int             $object_id The post ID to update the terms form.
     * @param WP_REST_Request $request   The request object with post and terms data.
     * @return null|WP_Error  WP_Error on an error assigning any of the terms, otherwise null.
     */
    protected function handle_terms( $object_id, $request ) {

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

        foreach ( $taxonomies as $taxonomy ) {
            $base = $this->get_taxonomy_rest_base( $taxonomy );

            if ( ! isset( $request[ $base ] ) ) {
                continue;
            }

            // We could use LLMS_Post_Model::set_terms() but it doesn't return a WP_Error which can be useful here.
            $result = wp_set_object_terms( $object_id, $request[ $base ], $taxonomy->name );
            if ( is_wp_error( $result ) ) {
                return $result;
            }
        }
    }

    /**
     * Checks whether current user can assign all terms sent with the current request.
     *
     * Heavily based on WP_REST_Posts_Controller::check_assign_terms_permission().
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0-beta.3 Filter taxonomies by `show_in_llms_rest` property instead of `public`.
     *
     * @param WP_REST_Request $request The request object with post and terms data.
     * @return bool Whether the current user can assign the provided terms.
     */
    protected function check_assign_terms_permission( $request ) {
        $taxonomies = wp_list_filter( get_object_taxonomies( $this->post_type, 'objects' ), array( 'show_in_llms_rest' => true ) );
        foreach ( $taxonomies as $taxonomy ) {
            $base = $this->get_taxonomy_rest_base( $taxonomy );

            if ( ! isset( $request[ $base ] ) ) {
                continue;
            }

            foreach ( $request[ $base ] as $term_id ) {
                // Invalid terms will be rejected later.
                if ( ! get_term( $term_id, $taxonomy->name ) ) {
                    continue;
                }

                if ( ! current_user_can( 'assign_term', (int) $term_id ) ) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Maps a taxonomy name to the relative rest base
     *
     * @since 1.0.0-beta.1
     *
     * @param object $taxonomy The taxonomy object.
     * @return string The taxonomy rest base.
     */
    protected function get_taxonomy_rest_base( $taxonomy ) {

        return ! empty( $taxonomy->rest_base ) ? $taxonomy->rest_base : $taxonomy->name;

    }

    /**
     * Checks if a post can be edited.
     *
     * @since 1.0.0-beta.1
     *
     * @return bool Whether the post can be created
     */
    protected function check_create_permission() {

        $post_type = get_post_type_object( $this->post_type );
        return current_user_can( $post_type->cap->publish_posts );

    }

    /**
     * Checks if an llms post can be edited.
     *
     * @since 1.0.0-beta.1
     *
     * @param LLMS_Post_Model $object Optional. The LLMS_Post_model object. Default null.
     * @return bool Whether the post can be edited.
     */
    protected function check_update_permission( $object = null ) {

        $post_type = get_post_type_object( $this->post_type );
        return is_null( $object ) ? current_user_can( $post_type->cap->edit_posts ) : current_user_can( $post_type->cap->edit_post, $object->get( 'id' ) );

    }

    /**
     * Checks if an llms post can be deleted.
     *
     * @since 1.0.0-beta.1
     *
     * @param LLMS_Post_Model $object The LLMS_Post_model object.
     * @return bool Whether the post can be deleted.
     */
    protected function check_delete_permission( $object ) {

        $post_type = get_post_type_object( $this->post_type );
        return current_user_can( $post_type->cap->delete_post, $object->get( 'id' ) );

    }

    /**
     * Checks if an llms post can be read.
     *
     * @since 1.0.0-beta.1
     * @since 1.0.0 Fix fatals when searching for llms post type based resources
     *                  but the query post type parameter is forced to be something else.
     *
     * @param LLMS_Post_Model $object The LLMS_Post_model object.
     * @return bool Whether the post can be read.
     */
    protected function check_read_permission( $object ) {

        if ( is_wp_error( $object ) ) {
            return false;
        }

        $post_type = get_post_type_object( $this->post_type );
        $status    = $object->get( 'status' );
        $id        = $object->get( 'id' );
        $wp_post   = $object->get( 'post' );

        // Is the post readable?
        if ( 'publish' === $status || current_user_can( $post_type->cap->read_post, $id ) ) {
            return true;
        }

        $post_status_obj = get_post_status_object( $status );
        if ( $post_status_obj && $post_status_obj->public ) {
            return true;
        }

        // Can we read the parent if we're inheriting?
        if ( 'inherit' === $status && $wp_post->post_parent > 0 ) {
            $parent = get_post( $wp_post->post_parent );
            if ( $parent ) {
                return $this->check_read_permission( $parent );
            }
        }

        /*
         * If there isn't a parent, but the status is set to inherit, assume
         * it's published (as per get_post_status()).
         */
        if ( 'inherit' === $status ) {
            return true;
        }

        return false;

    }


    /**
     * Checks if the user can access password-protected content.
     *
     * @since 1.0.0-beta.1
     *
     * @param LLMS_Post_Model $object  The LLMS_Post_model object.
     * @param WP_REST_Request $request Request data to check.
     * @return bool True if the user can access password-protected content, otherwise false.
     */
    public function can_access_password_content( $object, $request ) {

        if ( empty( $object->get( 'password' ) ) ) {
            // No filter required.
            return false;
        }

        // Edit context always gets access to password-protected posts.
        if ( 'edit' === $request['context'] ) {
            return true;
        }

        // No password, no auth.
        if ( empty( $request['password'] ) ) {
            return false;
        }

        // Double-check the request password.
        return hash_equals( $object->get( 'password' ), $request['password'] );
    }

    /**
     * Get the llms post model class from the controller post type.
     *
     * @since 1.0.0-beta.9
     *
     * @return string|bool The llms post model class name if it exists or FALSE if it doesn't.
     */
    protected function llms_post_class_from_post_type() {

        if ( isset( $this->llms_post_class ) ) {
            return $this->llms_post_class;
        }

        $post_type = explode( '_', str_replace( 'llms_', '', $this->post_type ) );
        $class     = 'LLMS';

        foreach ( $post_type as $part ) {
            $class .= '_' . ucfirst( $part );
        }

        if ( class_exists( $class ) ) {
            $this->llms_post_class = $class;
        } else {
            $this->llms_post_class = false;
        }

        return $this->llms_post_class;
    }

    /**
     * Sanitizes and validates the list of post statuses, including whether the user can query private statuses
     *
     * Heavily based on the WordPress  WP_REST_Posts_Controller::sanitize_post_statuses().
     *
     * @since 1.0.0-beta.19
     *
     * @param string|array    $statuses  One or more post statuses.
     * @param WP_REST_Request $request   Full details about the request.
     * @param string          $parameter Additional parameter to pass to validation.
     * @return array|WP_Error A list of valid statuses, otherwise WP_Error object.
     */
    public function sanitize_post_statuses( $statuses, $request, $parameter ) {
        $statuses = wp_parse_slug_list( $statuses );

        $attributes     = $request->get_attributes();
        $default_status = $attributes['args']['status']['default'];

        foreach ( $statuses as $status ) {
            if ( $status === $default_status ) {
                continue;
            }

            $post_type_obj = get_post_type_object( $this->post_type );

            if ( current_user_can( $post_type_obj->cap->edit_posts ) || 'private' === $status && current_user_can( $post_type_obj->cap->read_private_posts ) ) {
                $result = rest_validate_request_arg( $status, $request, $parameter );
                if ( is_wp_error( $result ) ) {
                    return $result;
                }
            } else {
                return llms_rest_authorization_required_error( __( 'Status is forbidden.', 'lifterlms' ) );
            }
        }

        return $statuses;
    }

}