gocodebox/lifterlms-rest

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

Summary

Maintainability
D
2 days
Test Coverage
A
90%
<?php
/**
 * REST Access Plans Controller
 *
 * @package LifterLMS_REST/Classes/Controllers
 *
 * @since 1.0.0-beta.18
 * @version 1.0.0-beta.27
 */

defined( 'ABSPATH' ) || exit;

/**
 * LLMS_REST_Access_Plans_Controller class
 *
 * @since 1.0.0-beta.18
 */
class LLMS_REST_Access_Plans_Controller extends LLMS_REST_Posts_Controller {

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

    /**
     * Route base.
     *
     * @var string
     */
    protected $rest_base = 'access-plans';

    /**
     * Get the Access Plan's schema, conforming to JSON Schema.
     *
     * @since 1.0.0-beta.18
     * @since 1.0.0-beta.27 Do not fire the llms_rest_access_plan_item_schema filter, it'll be fired in `LLMS_REST_Controller::filter_item_schema()`.
     *
     * @return array
     */
    public function get_item_schema_base() {

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

        // Post properties to unset.
        $properties_to_unset = array(
            'comment_status',
            'excerpt',
            'featured_media',
            'password',
            'ping_status',
            'slug',
            'status',
        );

        foreach ( $properties_to_unset as $to_unset ) {
            unset( $schema['properties'][ $to_unset ] );
        }

        // The content is not required.
        unset( $schema['properties']['content']['required'] );

        $access_plan_properties = require LLMS_REST_API_PLUGIN_DIR . 'includes/server/schemas/schema-access-plans.php';

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

        return $schema;

    }

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

        $query_params = parent::get_collection_params();

        $query_params['post_id'] = array(
            'description'       => __( 'Retrieve access plans for a specific list of one or more posts. Accepts a course/membership id or comma separated list of course/membership ids.', 'lifterlms' ),
            'type'              => 'array',
            'items'             => array(
                'type' => 'integer',
            ),
            'validate_callback' => 'rest_validate_request_arg',
        );

        return $query_params;
    }

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

    /**
     * Whether the delete should be forced
     *
     * @since 1.0.0-beta.18
     *
     * @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 true;
    }

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

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

        $can_create = parent::create_item_permissions_check( $request );

        // If current user cannot create the item because of authorization, check if the current user can edit the "parent" course/membership.
        $can_create = $this->related_product_permissions_check( $can_create, $request );

        return is_wp_error( $can_create ) ? $can_create : $this->allow_request_when_access_plan_limit_not_reached( $request );
    }

    /**
     * Check if a given request has access to update an item.
     *
     * @since 1.0.0-beta.18
     * @since 1.0.0-beta.20 Call to private method `block_request_when_access_plan_limit` replaced with a call to the new `allow_request_when_access_plan_limit_not_reached` method.
     *
     * @param  WP_REST_Request $request Full details about the request.
     * @return WP_Error|boolean
     */
    public function update_item_permissions_check( $request ) {

        $can_update = parent::update_item_permissions_check( $request );

        // If current user cannot edit the item because of authorization, check if the current user can edit the "parent" course/membership.
        $can_update = $this->related_product_permissions_check( $can_update, $request );

        return is_wp_error( $can_update ) ? $can_update : $this->allow_request_when_access_plan_limit_not_reached( $request );

    }

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

        $can_delete = parent::delete_item_permissions_check( $request );

        // If current user cannot delete the item because of authorization, check if the current user can edit the "parent" course/membership.
        return $this->related_product_permissions_check( $can_delete, $request );

    }

    /**
     * Prepare links for the request
     *
     * @since 1.0.0-beta.18
     *
     * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance.
     * @param WP_REST_Request  $request     Request object.
     * @return array Links for the given object.
     */
    protected function prepare_links( $access_plan, $request ) {

        $links = parent::prepare_links( $access_plan, $request );
        unset( $links['content'] );

        $links['post'] = array(
            'href' => rest_url(
                sprintf(
                    '%s/%s/%s',
                    'llms/v1',
                    'course' === $access_plan->get_product_type() ? 'courses' : 'memberships',
                    $access_plan->get( 'product_id' )
                )
            ),
        );

        // Membership restrictions.
        if ( $access_plan->has_availability_restrictions() ) {
            $links['restrictions'] = array(
                'href' => rest_url(
                    sprintf(
                        '%s/%s?include=%s',
                        'llms/v1',
                        'memberships',
                        implode( ',', $access_plan->get_array( 'availability_restrictions' ) )
                    )
                ),
            );
        }

        /**
         * Filters the access plan's links.
         *
         * @since 1.0.0-beta.18
         *
         * @param array            $links       Links for the given access plan.
         * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance.
         */
        return apply_filters( 'llms_rest_access_plan_links', $links, $access_plan );

    }

    /**
     * Prepare a single object output for response.
     *
     * @since 1.0.0-beta.18
     * @since 1.0.0-beta.20 Fixed return format of the `access_expires` property.
     *                      Fixed sale date properties.
     * @since 1.0.0-beta-24 Fixed `availability_restrictions` never returned.
     *
     * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance.
     * @param WP_REST_Request  $request     Full details about the request.
     * @return array
     */
    protected function prepare_object_for_response( $access_plan, $request ) {

        $data    = parent::prepare_object_for_response( $access_plan, $request );
        $context = $request->get_param( 'context' );

        // Price.
        $data['price'] = $access_plan->is_free() ? 0 : $access_plan->get_price( 'price', array(), 'float' );

        // Access expiration.
        $data['access_expiration'] = $access_plan->get( 'access_expiration' );

        // Access expires date.
        if ( 'limited-date' === $data['access_expiration'] || 'edit' === $context ) {
            $data['access_expires'] = $access_plan->get_date( 'access_expires', 'Y-m-d H:i:s' );
        }

        // Access length and period.
        if ( 'limited-period' === $data['access_expiration'] || 'edit' === $context ) {
            $data['access_length'] = $access_plan->get( 'access_length' );
            $data['access_period'] = $access_plan->get( 'access_period' );
        }

        // Availability restrictions, only returned for courses.
        if ( 'course' === $access_plan->get_product_type() ) {
            $data['availability_restrictions'] = $access_plan->has_availability_restrictions()
                ?
                array_map( 'absint', $access_plan->get_array( 'availability_restrictions' ) )
                :
                array();
        }

        // Enroll text.
        $data['enroll_text'] = $access_plan->get_enroll_text();

        // Frequency.
        $data['frequency'] = $access_plan->get( 'frequency' );

        // Length and period.
        if ( 0 < $data['frequency'] || 'edit' === $context ) {
            $data['length'] = $access_plan->get( 'length' );
            $data['period'] = $access_plan->get( 'period' );
        }

        // Post ID.
        $data['post_id'] = $access_plan->get( 'product_id' );

        // Redirect forced.
        if ( ! empty( $data['availability_restrictions'] ) || 'edit' === $context ) {
            $data['redirect_forced'] = llms_parse_bool( $access_plan->get( 'checkout_redirect_forced' ) );
        }

        // Redirect type.
        $data['redirect_type'] = $access_plan->get( 'checkout_redirect_type' );

        // Redirect page.
        if ( 'page' === $data['redirect_type'] || 'edit' === $context ) {
            $data['redirect_page'] = $access_plan->get( 'checkout_redirect_page' );
        }

        // Redirect url.
        if ( 'url' === $data['redirect_type'] || 'edit' === $context ) {
            $data['redirect_url'] = $access_plan->get( 'checkout_redirect_url' );
        }

        // Permalink.
        $data['permalink'] = $access_plan->get_checkout_url( false );

        // Sale enabled.
        $data['sale_enabled'] = llms_parse_bool( $access_plan->get( 'on_sale' ) );

        // Sale start/end and price.
        if ( $data['sale_enabled'] || 'edit' === $context ) {
            $data['sale_date_start'] = $access_plan->get_date( 'sale_start', 'Y-m-d H:i:s' );
            $data['sale_date_end']   = $access_plan->get_date( 'sale_end', 'Y-m-d H:i:s' );
            $data['sale_price']      = $access_plan->get_price( 'sale_price', array(), 'float' );
        }

        // SKU.
        $data['sku'] = $access_plan->get( 'sku' );

        // Trial.
        $data['trial_enabled'] = $access_plan->has_trial();

        if ( $data['trial_enabled'] || 'edit' === $context ) {
            $data['trial_length'] = $access_plan->get( 'trial_length' );
            $data['trial_period'] = $access_plan->get( 'trial_period' );
            $data['trial_price']  = $access_plan->get_price( 'trial_price', array(), 'float' );
        }

        // Visibility.
        $data['visibility'] = $access_plan->get_visibility();

        /**
         * Filters the access plan data for a response.
         *
         * @since 1.0.0-beta.18
         *
         * @param array            $data        Array of lesson properties prepared for response.
         * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance.
         * @param WP_REST_Request  $request     Full details about the request.
         */
        $data = apply_filters( 'llms_rest_prepare_access_plan_object_response', $data, $access_plan, $request );

        return $data;
    }

    /**
     * Format query arguments to retrieve a collection of objects
     *
     * @since 1.0.0-beta.18
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return array|WP_Error
     */
    protected function prepare_collection_query_args( $request ) {

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

        // Filter by post ID.
        if ( ! empty( $request['post_id'] ) ) {
            $query_args = array_merge(
                $query_args,
                array(
                    'meta_query' => array(
                        array(
                            'key'     => '_llms_product_id',
                            'value'   => $request['post_id'],
                            'compare' => 'IN',
                        ),
                    ),
                )
            );
        }

        return $query_args;
    }

    /**
     * Prepares a single post for create or update
     *
     * @since 1.0.0-beta.18
     *
     * @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 = parent::prepare_item_for_database( $request );
        if ( is_wp_error( $prepared_item ) ) {
            return $prepared_item;
        }

        $schema = $this->get_item_schema();

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

        // Post id.
        if ( ! empty( $schema['properties']['post_id'] ) && isset( $request['post_id'] ) ) {
            $prepared_item['product_id'] = $request['post_id'];
        }

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

        /**
         * Filters the access plan data before inserting in the db
         *
         * @since 1.0.0-beta.18
         *
         * @param array           $prepared_item Array of access plan item properties prepared for database.
         * @param WP_REST_Request $request       Full details about the request.
         * @param array           $schema        The item schema.
         */
        $prepared_item = apply_filters( 'llms_rest_pre_insert_access_plan', $prepared_item, $request, $schema );

        return $prepared_item;
    }

    /**
     * Updates an existing single LLMS_Access_Plan in the database.
     *
     * This method should be used for access plan properties that require the access plan id in order to be saved in the database.
     *
     * @since 1.0.0-beta.18
     * @since 1.0.0-beta-24 Fixed reference to a non-existent schema property: visibiliy in place of visibility.
     *                      Fixed issue that prevented updating the access plan `redirect_forced` property.
     *                      Better handling of the availability_restrictions.
     * @since 1.0.0-beta.25 Allow updating meta with the same value as the stored one.
     *
     * @param LLMS_Access_Plan $access_plan   LLMS Access Plan instance.
     * @param WP_REST_Request  $request       Full details about the request.
     * @param array            $schema        The item schema.
     * @param array            $prepared_item Array.
     * @param bool             $creating      Optional. Whether we're in creation or update phase. Default true (create).
     * @return bool|WP_Error True on success or false if nothing to update, WP_Error object if something went wrong during the update.
     */
    protected function update_additional_object_fields( $access_plan, $request, $schema, $prepared_item, $creating = true ) {

        $error = new WP_Error();

        // Will contain the properties to set.
        $to_set = array();

        // Access expiration.
        if ( ! empty( $schema['properties']['access_expiration'] ) && isset( $request['access_expiration'] ) ) {
            $to_set['access_expiration'] = $request['access_expiration'];
        }

        // Access expires.
        if ( ! empty( $schema['properties']['access_expires'] ) && isset( $request['access_expires'] ) ) {
            $access_expires           = rest_parse_date( $request['access_expires'] );
            $to_set['access_expires'] = empty( $access_expires ) ? '' : date_i18n( 'Y-m-d H:i:s', $access_expires );
        }

        // Access length.
        if ( ! empty( $schema['properties']['access_length'] ) && isset( $request['access_length'] ) ) {
            $to_set['access_length'] = $request['access_length'];
        }

        // Access period.
        if ( ! empty( $schema['properties']['access_period'] ) && isset( $request['access_period'] ) ) {
            $to_set['access_period'] = $request['access_period'];
        }

        // Redirect.
        if ( ! empty( $schema['properties']['redirect_type'] ) && isset( $request['redirect_type'] ) ) {
            $to_set['checkout_redirect_type'] = $request['redirect_type'];
        }

        // Redirect page.
        if ( ! empty( $schema['properties']['redirect_page'] ) && isset( $request['redirect_page'] ) ) {
            $redirect_page = get_post( $request['redirect_page'] );
            if ( $redirect_page && is_a( $redirect_page, 'WP_Post' ) ) {
                $to_set['checkout_redirect_page'] = $request['redirect_page']; // maybe allow only published pages?
            }
        }

        // Redirect url.
        if ( ! empty( $schema['properties']['redirect_url'] ) && isset( $request['redirect_url'] ) ) {
            $to_set['checkout_redirect_url'] = $request['redirect_url'];
        }

        // Price.
        if ( ! empty( $schema['properties']['price'] ) && isset( $request['price'] ) ) {
            $to_set['price'] = $request['price'];
        }

        // Sale enabled.
        if ( ! empty( $schema['properties']['sale_enabled'] ) && isset( $request['sale_enabled'] ) ) {
            $to_set['on_sale'] = $request['sale_enabled'] ? 'yes' : 'no';
        }

        // Sale dates.
        if ( ! empty( $schema['properties']['sale_date_start'] ) && isset( $request['sale_date_start'] ) ) {
            $sale_date_start      = rest_parse_date( $request['sale_date_start'] );
            $to_set['sale_start'] = empty( $sale_date_start ) ? '' : date_i18n( 'Y-m-d H:i:s', $sale_date_start );
        }

        if ( ! empty( $schema['properties']['sale_date_end'] ) && isset( $request['sale_date_end'] ) ) {
            $sale_date_end      = rest_parse_date( $request['sale_date_end'] );
            $to_set['sale_end'] = empty( $sale_date_end ) ? '' : date_i18n( 'Y-m-d H:i:s', $sale_date_end );
        }
        // Sale price.
        if ( ! empty( $schema['properties']['sale_price'] ) && isset( $request['sale_price'] ) ) {
            $to_set['sale_price'] = $request['sale_price'];
        }

        // Trial enabled.
        if ( ! empty( $schema['properties']['trial_enabled'] ) && isset( $request['trial_enabled'] ) ) {
            $to_set['trial_offer'] = $request['trial_enabled'] ? 'yes' : 'no';
        }

        // Trial Length.
        if ( ! empty( $schema['properties']['trial_length'] ) && isset( $request['trial_length'] ) ) {
            $to_set['trial_length'] = $request['trial_length'];
        }
        // Trial Period.
        if ( ! empty( $schema['properties']['trial_period'] ) && isset( $request['trial_period'] ) ) {
            $to_set['trial_period'] = $request['trial_period'];
        }
        // Trial price.
        if ( ! empty( $schema['properties']['trial_price'] ) && isset( $request['trial_price'] ) ) {
            $to_set['trial_price'] = $request['trial_price'];
        }

        // Availability restrictions.
        // If access plan related post type is not a course, set availability to 'open' and clean the `availability_restrictions` array.
        if ( 'course' !== $access_plan->get_product_type() ) {
            $to_set['availability']              = 'open';
            $to_set['availability_restrictions'] = array();
        } elseif ( ! empty( $schema['properties']['availability_restrictions'] ) && isset( $request['availability_restrictions'] ) ) {
            $to_set['availability_restrictions'] = $request['availability_restrictions'];
            // If availability restrictions supplied is not empty, set `availability` to 'members'.
            $to_set['availability'] = ! empty( $to_set['availability_restrictions'] ) ? 'members' : 'open';
        }

        // Redirect forced.
        if ( ! empty( $schema['properties']['redirect_forced'] ) && isset( $request['redirect_forced'] ) ) {
            $to_set['checkout_redirect_forced'] = $request['redirect_forced'] ? 'yes' : 'no';
        }

        // Frequency.
        if ( ! empty( $schema['properties']['frequency'] ) && isset( $request['frequency'] ) ) {
            $to_set['frequency'] = $request['frequency'];
        }

        // Length.
        if ( ! empty( $schema['properties']['length'] ) && isset( $request['length'] ) ) {
            $to_set['length'] = $request['length'];
        }
        // Period.
        if ( ! empty( $schema['properties']['period'] ) && isset( $request['period'] ) ) {
            $to_set['period'] = $request['period'];
        }

        $this->handle_props_interdependency( $to_set, $access_plan, $creating );

        // Visibility.
        if ( ! empty( $schema['properties']['visibility'] ) && isset( $request['visibility'] ) ) {
            $visibility = $access_plan->set_visibility( $request['visibility'] );
            if ( is_wp_error( $visibility ) ) {
                return $visibility;
            }
        }

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

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

        return ! empty( $to_set ) || ! empty( $visibility );
    }

    /**
     * Handle properties interdependency
     *
     * @since 1.0.0-beta.18
     *
     * @param array            $to_set      Array of properties to be set.
     * @param LLMS_Access_Plan $access_plan LLMS Access Plan instance.
     * @param bool             $creating    Whether we're in creation or update phase.
     * @return void
     */
    private function handle_props_interdependency( &$to_set, $access_plan, $creating ) {

        // Access Plan properties as saved in the db.
        $saved_props = $access_plan->toArray();

        $this->add_subordinate_props( $to_set, $saved_props, $creating );

        $this->unset_subordinate_props( $to_set, $saved_props );

    }

    /**
     * Add all the properties which need to be set as consequence of another setting
     *
     * These properties must be compared to the saved value before updating, because if equal they will produce an error(see update_post_meta()).
     *
     * @since 1.0.0-beta.18
     * @since 1.0.0-beta-24 Cast `price` property to float.
     * @since 1.0.0-beta.25 Allow updating meta with the same value as the stored one.
     *
     * @param array $to_set      Array of properties to be set.
     * @param array $saved_props Array of LLMS_Access_Plan properties as saved in the db.
     * @param bool  $creating    Whether we're in creation or update phase.
     * @return void
     */
    private function add_subordinate_props( &$to_set, $saved_props, $creating ) {

        $subordinate_props = array();

        // Merge new properties to set and saved props.
        $props = wp_parse_args( $to_set, $saved_props );

        // Paid plan.
        if ( $props['price'] > 0 ) {

            $subordinate_props['is_free'] = 'no';

            // One-time (no trial).
            if ( 0 === $props['frequency'] ) {
                $subordinate_props['trial_offer'] = 'no';
            }
        } else {

            $subordinate_props['is_free']     = 'yes';
            $subordinate_props['price']       = 0.0;
            $subordinate_props['frequency']   = 0;
            $subordinate_props['on_sale']     = 'no';
            $subordinate_props['trial_offer'] = 'no';

        }

        $to_set = array_merge( $to_set, $subordinate_props );

    }

    /**
     * Remove all the properties that do not need to be set, based on other properties
     *
     * @since 1.0.0-beta.18
     *
     * @param array $to_set      Array of properties to be set.
     * @param array $saved_props Array of LLMS_Access_Plan properties as saved in the db.
     * @return void
     */
    private function unset_subordinate_props( &$to_set, $saved_props ) {

        // Merge new properties to set and saved props.
        $props = wp_parse_args( $to_set, $saved_props );

        // No need to create/update recurring props when it's a 1-time payment.
        if ( 0 === $props['frequency'] ) {
            unset( $to_set['length'], $to_set['period'] );
        }

        // No need to create/update trial props when no trial enabled.
        if ( ! llms_parse_bool( $props['trial_offer'] ) ) {
            unset( $to_set['trial_price'], $to_set['trial_length'], $to_set['trial_period'] );
        }

        // No need to create/update sale props when not on sale.
        if ( ! llms_parse_bool( $props['on_sale'] ) ) {
            unset( $to_set['sale_price'], $to_set['sale_end'], $to_set['sale_start'] );
        }

        // Unset redirect props based on redirect settings.
        if ( 'url' === $props['checkout_redirect_type'] ) {
            unset( $to_set['checkout_redirect_page'] );
        } elseif ( 'page' === $props['checkout_redirect_type'] ) {
            unset( $to_set['checkout_redirect_url'] );
        } else {
            unset( $to_set['checkout_redirect_url'], $to_set['checkout_redirect_page'] );
        }

        // Unset expiration props based on expiration settings.
        if ( 'lifetime' === $props['access_expiration'] ) {
            unset( $to_set['access_expires'], $to_set['access_length'], $to_set['access_period'] );
        } elseif ( 'limited-date' === $props['access_expiration'] ) {
            unset( $to_set['access_length'], $to_set['access_period'] );
        } elseif ( 'limited-period' === $props['access_expiration'] ) {
            unset( $to_set['access_expires'] );
        }
    }

    /**
     * Check if the current user, who has no permissions to manipulate the access plan post, can edit its related product.
     *
     * @since 1.0.0-beta.18
     * @since 1.0.0-beta.20 Made sure either we're creating or updating prior to check related product's permissions.
     *
     * @param boolean|WP_Error $has_permissions Whether or not the current user has the permission to manipulate the resource.
     * @param WP_REST_Request  $request         Full details about the request.
     * @return boolean|WP_Error
     */
    private function related_product_permissions_check( $has_permissions, $request ) {

        if ( llms_rest_is_authorization_required_error( $has_permissions ) ) {

            // `id` required on "reading/updating", `post_id` required on "creating".
            if ( empty( $request['id'] ) && empty( $request['post_id'] ) ) {
                return $has_permissions;
            }

            $product_id = isset( $request['id'] ) /* not creation */ ? $this->get_object( (int) $request['id'] )->get( 'product_id' ) : (int) $request['post_id'];

            $product_post_type_object = get_post_type_object( get_post_type( $product_id ) );

            if ( current_user_can( $product_post_type_object->cap->edit_post, $product_id ) ) {
                $has_permissions = true;
            }
        }

        return $has_permissions;
    }

    /**
     * Allow request when the access plan limit per product is not reached.
     *
     * @since 1.0.0-beta.20
     * @since 1.0.0-beta-24 Made sure we can update an access plan of a product even if its access plan limit has already been reached.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error
     */
    private function allow_request_when_access_plan_limit_not_reached( $request ) {

        // `id` required on "reading/updating", `post_id` required on "creating".
        if ( empty( $request['id'] ) && empty( $request['post_id'] ) ) {
            return true;
        }

        $product_id           = isset( $request['post_id'] ) ? $request['post_id'] : $this->get_object( (int) $request['id'] )->get( 'product_id' );
        $product              = new LLMS_Product( $product_id );
        $limit                = $product->get_access_plan_limit();
        $product_access_plans = $product->get_access_plans( false, false );
        // Check whether we're updating an access plan, and whether this access plan was already a destination's product access plan,
        // otherwise we're either creating an access plan or moving the access plans from a product to a different one.
        $updating_product_access_plan = ! empty( $request['id'] ) && ! empty( $product_access_plans ) && in_array( $request['id'], wp_list_pluck( $product_access_plans, 'id' ), true );

        if ( ! $updating_product_access_plan && count( $product_access_plans ) >= $limit ) {

            return llms_rest_bad_request_error(
                sprintf(
                    // Translators: %1$d = access plans limit per product, %2$s access plan post type plural name, %3$s product post type singular name.
                    __( 'Only %1$d %2$s allowed per %3$s', 'lifterlms' ),
                    $limit,
                    strtolower( get_post_type_object( $this->post_type )->labels->name ),
                    strtolower( get_post_type_object( get_post_type( $product_id ) )->labels->singular_name )
                )
            );

        }

        return true;
    }

}