JezK
Edit File: ProductAttributeController.php
<?php namespace WeDevs\Dokan\REST; use WC_REST_Product_Attributes_V1_Controller; use WeDevs\Dokan\Product\ProductAttribute; use WP_REST_Response; use WP_REST_Server; use WP_Error; use WP_REST_Request; class ProductAttributeController extends WC_REST_Product_Attributes_V1_Controller { /** * Endpoint namespace. * * @var string */ protected $namespace = 'dokan/v1'; /** * Route base. * * @var string */ protected $rest_base = 'products/attributes'; /** * Attribute name. * * @var string */ protected $attribute = ''; /** * Register the routes for product attributes. */ public function register_routes() { parent::register_routes(); // REST API for Product attribute edit section. register_rest_route( $this->namespace, '/' . $this->rest_base . '/edit-product/(?P<id>[\d]+)', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_product_attribute' ), 'permission_callback' => array( $this, 'update_product_attribute_permissions_check' ), 'args' => $this->get_product_update_collection_params(), ), ) ); // REST API for setting Product default attribute. register_rest_route( $this->namespace, '/' . $this->rest_base . '/set-default/(?P<id>[\d]+)', array( array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array( $this, 'update_product_default_attribute' ), 'permission_callback' => array( $this, 'update_product_attribute_permissions_check' ), 'args' => $this->get_product_update_collection_params(), ), ) ); // REST API for lazily fetching / creating terms of a global attribute. register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[\d]+)/terms', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_attribute_terms' ), 'permission_callback' => array( $this, 'get_items_permissions_check' ), 'args' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'dokan-lite' ), 'type' => 'integer', 'required' => true, ), 'search' => array( 'description' => __( 'Limit results to terms matching a string.', 'dokan-lite' ), 'type' => 'string', ), 'page' => array( 'description' => __( 'Current page of the collection.', 'dokan-lite' ), 'type' => 'integer', 'default' => 1, ), 'per_page' => array( 'description' => __( 'Maximum number of terms to return in the result set.', 'dokan-lite' ), 'type' => 'integer', 'default' => 20, ), ), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create_attribute_term' ), 'permission_callback' => array( $this, 'create_attribute_term_permissions_check' ), 'args' => array( 'id' => array( 'description' => __( 'Attribute ID.', 'dokan-lite' ), 'type' => 'integer', 'required' => true, ), 'name' => array( 'description' => __( 'Term name.', 'dokan-lite' ), 'type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field', ), ), ), ) ); } /** * Check if a given request has access to read the attributes. * * @param WP_REST_Request $request Full details about the request. * * @return bool */ public function get_items_permissions_check( $request ) { return current_user_can( 'dokandar' ); } /** * Check if a given request has access to create a attribute. * * @param WP_REST_Request $request Full details about the request. * * @return bool */ public function create_item_permissions_check( $request ) { return current_user_can( 'dokan_add_product' ); } /** * Check if a given request has access to read a attribute. * * @param WP_REST_Request $request Full details about the request. * * @return WP_Error|bool */ public function get_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( 'dokan_rest_taxonomy_invalid', __( 'Resource does not exist.', 'dokan-lite' ), array( 'status' => 404 ) ); } return current_user_can( 'dokandar' ); } /** * Check if a given request has access to update a attribute. * * @param WP_REST_Request $request Full details about the request. * * @return WP_Error|bool */ public function update_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( 'dokan_rest_taxonomy_invalid', __( 'Resource does not exist.', 'dokan-lite' ), array( 'status' => 404 ) ); } return current_user_can( 'dokan_edit_product' ); } /** * Check if a given request has access to update a product attribute. * * @param WP_REST_Request $request Full details about the request. * * @return WP_Error|bool */ public function update_product_attribute_permissions_check( $request ) { // A vendor may only edit attributes on their own products; admins/shop managers are exempt. if ( ! current_user_can( 'manage_woocommerce' ) && ! dokan_is_product_author( absint( $request['id'] ) ) ) { return new WP_Error( 'dokan_rest_cannot_edit', __( 'Sorry, you are not allowed to edit this product.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); } return current_user_can( 'dokan_edit_product' ); } /** * Check if a given request has access to create a new attribute term. * * Creating terms is only allowed when vendors are permitted to add new * attributes from the selling options. * * @since 5.0.5 * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function create_attribute_term_permissions_check( $request ) { if ( 'on' !== dokan_get_option( 'add_new_attribute', 'dokan_selling', 'off' ) ) { return new WP_Error( 'dokan_rest_cannot_create', __( 'Sorry, you are not allowed to create new attribute terms.', 'dokan-lite' ), array( 'status' => rest_authorization_required_code() ) ); } return current_user_can( 'dokan_edit_product' ); } /** * Check if a given request has access to delete a attribute. * * @param WP_REST_Request $request Full details about the request. * * @return WP_Error|bool */ public function delete_item_permissions_check( $request ) { if ( ! $this->get_taxonomy( $request ) ) { return new WP_Error( 'woocommerce_rest_taxonomy_invalid', __( 'Resource does not exist.', 'dokan-lite' ), array( 'status' => 404 ) ); } return current_user_can( 'dokan_delete_product' ); } /** * Check if a given request has access batch create, update and delete items. * * @param WP_REST_Request $request Full details about the request. * * @return bool|WP_Error */ public function batch_items_permissions_check( $request ) { return current_user_can( 'dokandar' ); } /** * Get product attribute and term update collection params. * * @since 3.7.10 * * @return array */ public function get_product_update_collection_params() { return [ 'attributes' => [ 'description' => __( 'Attribute options.', 'dokan-lite' ), 'type' => 'array', 'required' => true, 'context' => [ 'edit' ], 'items' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'description' => __( 'Attribute id.', 'dokan-lite' ), 'type' => 'int', 'context' => [ 'edit' ], 'sanitize_callback' => 'absint', ], 'name' => [ 'description' => __( 'Attribute name.', 'dokan-lite' ), 'type' => 'string', 'context' => [ 'edit' ], 'sanitize_callback' => 'sanitize_text_field', ], 'visible' => [ 'description' => __( 'Attribute visible in product list page or not.', 'dokan-lite' ), 'type' => 'boolean', 'default' => false, 'context' => [ 'edit' ], 'sanitize_callback' => 'boolval', ], 'variation' => [ 'description' => __( 'Attribute is for variation or not.', 'dokan-lite' ), 'type' => 'boolean', 'default' => false, 'context' => [ 'edit' ], 'sanitize_callback' => 'boolval', ], 'options' => [ 'description' => __( 'Attribute values.', 'dokan-lite' ), 'type' => 'array', 'required' => true, 'context' => [ 'edit' ], ], ], ], ], ]; } /** * Update product attributes by a product id. * * @since 3.7.10 * * @param WP_Rest_Request $request * @return WP_Error|WP_REST_Response Rest Response */ public function update_product_attribute( $request ) { $product_id = isset( $request['id'] ) ? absint( wp_unslash( $request['id'] ) ) : 0; if ( empty( $product_id ) ) { return new WP_Error( 'invalid_product_id', __( 'Invalid product id.', 'dokan-lite' ), [ 'status' => 400 ] ); } $product = wc_get_product( $product_id ); if ( empty( $product ) ) { return new WP_Error( 'no_product_found', __( 'No product found.', 'dokan-lite' ), [ 'status' => 404 ] ); } $product_attribute = new ProductAttribute( $request['attributes'] ); $is_saved = $product_attribute->set( $product, true ); if ( ! $is_saved ) { return new WP_Error( 'product_bulk_attribute_terms_saved_failed', __( 'Failed to save product bulk attribute and terms. Please try again later.', 'dokan-lite' ), [ 'status' => 400 ] ); } return rest_ensure_response( $product_attribute->get( $product_id ) ); } /** * Update product default attributes by a product id. * * @since 3.7.10 * * @param WP_Rest_Request $request * * @return WP_Error|WP_REST_Response Rest Response */ public function update_product_default_attribute( $request ) { $product_id = isset( $request['id'] ) ? absint( wp_unslash( $request['id'] ) ) : 0; if ( empty( $product_id ) ) { return new WP_Error( 'invalid_product_id', __( 'Invalid product id.', 'dokan-lite' ), [ 'status' => 400 ] ); } $product = wc_get_product( $product_id ); if ( empty( $product ) ) { return new WP_Error( 'no_product_found', __( 'No product found.', 'dokan-lite' ), [ 'status' => 404 ] ); } $product_attribute = new ProductAttribute( $request['attributes'] ); $is_saved = $product_attribute->set_default( $product, true ); if ( ! $is_saved ) { return new WP_Error( 'product_default_attribute_saved_failed', __( 'Failed to save product default attribute and terms. Please try again later.', 'dokan-lite' ), [ 'status' => 400 ] ); } return rest_ensure_response( $is_saved ); } /** * Resolve a global attribute taxonomy from its attribute ID. * * @since 5.0.5 * * @param int $attribute_id Global attribute ID. * * @return string|WP_Error Taxonomy name, or error if the attribute is invalid. */ protected function get_attribute_taxonomy_by_id( $attribute_id ) { $taxonomy = wc_attribute_taxonomy_name_by_id( absint( $attribute_id ) ); if ( empty( $taxonomy ) || ! taxonomy_exists( $taxonomy ) ) { return new WP_Error( 'dokan_rest_attribute_invalid', __( 'Invalid attribute.', 'dokan-lite' ), [ 'status' => 404 ] ); } return $taxonomy; } /** * Format a term for the product editor response. * * @since 5.0.5 * * @param \WP_Term $term Term object. * * @return array */ protected function prepare_attribute_term( $term ) { return [ 'id' => $term->term_id, 'name' => $term->name, 'value' => $term->term_id, 'label' => $term->name, ]; } /** * Get terms of a global product attribute (searchable + paginated). * * Terms are loaded lazily by the product editor instead of being embedded * into the form schema, so stores with very large attribute taxonomies do * not exhaust memory while building the editor payload. * * @since 5.0.5 * * @param WP_REST_Request $request Full details about the request. * * @return WP_Error|WP_REST_Response */ public function get_attribute_terms( $request ) { $taxonomy = $this->get_attribute_taxonomy_by_id( $request['id'] ); if ( is_wp_error( $taxonomy ) ) { return $taxonomy; } $per_page = absint( $request['per_page'] ); $per_page = $per_page > 0 ? $per_page : 20; $page = max( 1, absint( $request['page'] ) ); $search = isset( $request['search'] ) ? sanitize_text_field( wp_unslash( $request['search'] ) ) : ''; $args = [ 'taxonomy' => $taxonomy, 'hide_empty' => false, 'number' => $per_page, 'offset' => ( $page - 1 ) * $per_page, 'orderby' => 'name', 'order' => 'ASC', ]; if ( '' !== $search ) { $args['search'] = $search; } $terms = get_terms( $args ); if ( is_wp_error( $terms ) ) { return rest_ensure_response( [] ); } return rest_ensure_response( array_map( [ $this, 'prepare_attribute_term' ], $terms ) ); } /** * Create a new term for a global product attribute. * * @since 5.0.5 * * @param WP_REST_Request $request Full details about the request. * * @return WP_Error|WP_REST_Response */ public function create_attribute_term( $request ) { $taxonomy = $this->get_attribute_taxonomy_by_id( $request['id'] ); if ( is_wp_error( $taxonomy ) ) { return $taxonomy; } $name = sanitize_text_field( wp_unslash( $request['name'] ) ); if ( '' === $name ) { return new WP_Error( 'dokan_rest_term_name_required', __( 'Term name is required.', 'dokan-lite' ), [ 'status' => 400 ] ); } // Return the existing term instead of erroring when the term already exists. $existing = get_term_by( 'name', $name, $taxonomy ); if ( $existing && ! is_wp_error( $existing ) ) { return rest_ensure_response( $this->prepare_attribute_term( $existing ) ); } $inserted = wp_insert_term( $name, $taxonomy ); if ( is_wp_error( $inserted ) ) { return new WP_Error( 'dokan_rest_cannot_create_term', $inserted->get_error_message(), [ 'status' => 400 ] ); } $term = get_term( $inserted['term_id'], $taxonomy ); if ( ! $term || is_wp_error( $term ) ) { return new WP_Error( 'dokan_rest_cannot_create_term', __( 'Failed to create attribute term.', 'dokan-lite' ), [ 'status' => 400 ] ); } return rest_ensure_response( $this->prepare_attribute_term( $term ) ); } }