Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/dokan-lite/assets/js/components.asset.php
+++ b/dokan-lite/assets/js/components.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'aeeca4e1bc99c4509a21');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'f0e90796089291549e77');
--- a/dokan-lite/assets/js/dokan-admin-dashboard.asset.php
+++ b/dokan-lite/assets/js/dokan-admin-dashboard.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-product-editor-utils', 'dokan-react-components', 'dokan-stores-core', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-stores-vendors', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'ed3689c282f6a28ab8c7');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-product-editor-utils', 'dokan-react-components', 'dokan-stores-core', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-stores-vendors', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'e1e36404032ded246205');
--- a/dokan-lite/assets/js/dokan-tailwind.asset.php
+++ b/dokan-lite/assets/js/dokan-tailwind.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array(), 'version' => 'cad1dd82144b4683a743');
+<?php return array('dependencies' => array(), 'version' => 'a4b84f8ce2cf8b9c4b95');
--- a/dokan-lite/assets/js/frontend.asset.php
+++ b/dokan-lite/assets/js/frontend.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-product-editor-utils', 'dokan-react-components', 'dokan-stores-core', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => 'a6eb66e5fe137882c8cf');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-product-editor-utils', 'dokan-react-components', 'dokan-stores-core', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-plugins', 'wp-url'), 'version' => '352100595abea732cd55');
--- a/dokan-lite/assets/js/product-editor-store.asset.php
+++ b/dokan-lite/assets/js/product-editor-store.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('react-jsx-runtime', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-data'), 'version' => '6874793a775add0cf825');
+<?php return array('dependencies' => array('react-jsx-runtime', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-data', 'wp-i18n'), 'version' => 'e34f97aeb2a6877d7c54');
--- a/dokan-lite/assets/js/product-editor-utils.asset.php
+++ b/dokan-lite/assets/js/product-editor-utils.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-react-components', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'f7258d27b830040088c8');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-react-components', 'dokan-stores-product-categories', 'dokan-stores-product-editor', 'dokan-stores-products', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'aa653a2708d298468842');
--- a/dokan-lite/assets/js/utilities.asset.php
+++ b/dokan-lite/assets/js/utilities.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('wc-csv', 'wc-date'), 'version' => 'bf90477a237e82cf6a73');
+<?php return array('dependencies' => array('wc-csv', 'wc-date', 'wp-i18n'), 'version' => '59071d2a67f4bf914639');
--- a/dokan-lite/assets/js/vendor-dashboard/layout/index.asset.php
+++ b/dokan-lite/assets/js/vendor-dashboard/layout/index.asset.php
@@ -1 +1 @@
-<?php return array('dependencies' => array('dokan-hooks', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'c408fed91d629d8b27ff');
+<?php return array('dependencies' => array('dokan-hooks', 'dokan-utilities', 'lodash', 'react', 'react-dom', 'react-jsx-runtime', 'wc-components', 'wc-csv', 'wc-date', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-date', 'wp-dom-ready', 'wp-element', 'wp-hooks', 'wp-html-entities', 'wp-i18n', 'wp-url'), 'version' => 'c0fd74b925719a7bd393');
--- a/dokan-lite/dokan-class.php
+++ b/dokan-lite/dokan-class.php
@@ -27,7 +27,7 @@
*
* @var string
*/
- public $version = '5.0.4';
+ public $version = '5.0.5';
/**
* Instance of self
--- a/dokan-lite/dokan.php
+++ b/dokan-lite/dokan.php
@@ -3,7 +3,7 @@
* Plugin Name: Dokan
* Plugin URI: https://dokan.co/wordpress/
* Description: An e-commerce marketplace plugin for WordPress. Powered by WooCommerce and weDevs.
- * Version: 5.0.4
+ * Version: 5.0.5
* Author: Dokan Inc.
* Author URI: https://dokan.co/wordpress/
* Text Domain: dokan-lite
--- a/dokan-lite/includes/Abstracts/DokanRESTController.php
+++ b/dokan-lite/includes/Abstracts/DokanRESTController.php
@@ -293,10 +293,14 @@
* @return array
*/
protected function prepare_objects_query( $request ) {
+ // Vendors are always scoped to their own objects; only a store admin may target another vendor via the id param.
+ $is_store_admin = current_user_can( dokan_admin_menu_capability() );
+ $can_target_other_vendor = $is_store_admin && isset( $request['id'] );
+
$args = array();
$args['fields'] = 'ids';
$args['post_status'] = ! isset( $request['post_status'] ) ? $this->post_status : $request['post_status'];
- $args['author'] = ! isset( $request['id'] ) ? dokan_get_current_user_id() : $request['id'];
+ $args['author'] = $can_target_other_vendor ? (int) $request['id'] : dokan_get_current_user_id();
$args['offset'] = $request['offset'];
$args['order'] = $request['order'];
$args['orderby'] = $request['orderby'];
--- a/dokan-lite/includes/Admin/Menu.php
+++ b/dokan-lite/includes/Admin/Menu.php
@@ -30,7 +30,7 @@
public function add_admin_menu() {
global $submenu;
- $capability = dokana_admin_menu_capability();
+ $capability = dokan_admin_menu_capability();
if ( ! current_user_can( $capability ) ) {
return;
}
--- a/dokan-lite/includes/Product/Hooks.php
+++ b/dokan-lite/includes/Product/Hooks.php
@@ -129,12 +129,13 @@
}
$output .= '<li>';
- $output .= '<a href="' . get_post_permalink( $result->ID ) . '">';
+ // Escape every vendor-controlled value below before it is injected into the AJAX search markup.
+ $output .= '<a href="' . esc_url( get_post_permalink( $result->ID ) ) . '">';
$output .= '<div class="dokan-ls-product-image">';
$output .= '<img src="' . $get_product_image . '">';
$output .= '</div>';
$output .= '<div class="dokan-ls-product-data">';
- $output .= '<h3>' . $get_name . '</h3>';
+ $output .= '<h3>' . esc_html( $get_name ) . '</h3>';
if ( ! empty( $price ) ) {
$output .= '<div class="product-price">';
@@ -150,15 +151,15 @@
foreach ( $categories as $category ) {
if ( $category->parent ) {
$parent = get_term_by( 'id', $category->parent, 'product_cat' );
- $output .= '<span>' . $parent->name . '</span>';
+ $output .= '<span>' . esc_html( $parent->name ) . '</span>';
}
- $output .= '<span>' . $category->name . '</span>';
+ $output .= '<span>' . esc_html( $category->name ) . '</span>';
}
$output .= '</div>';
}
if ( ! empty( $sku ) ) {
- $output .= '<div class="dokan-ls-product-sku">' . esc_html__( 'SKU:', 'dokan-lite' ) . ' ' . $sku . '</div>';
+ $output .= '<div class="dokan-ls-product-sku">' . esc_html__( 'SKU:', 'dokan-lite' ) . ' ' . esc_html( $sku ) . '</div>';
}
$output .= '</div>';
--- a/dokan-lite/includes/Product/functions.php
+++ b/dokan-lite/includes/Product/functions.php
@@ -484,7 +484,7 @@
if ( current_user_can( 'dokan_edit_product' ) ) {
$row_action['edit'] = [
'title' => __( 'Edit', 'dokan-lite' ),
- 'url' => $edit_url = '/dashboard/new/#products/' . $product_id . '/edit',
+ 'url' => dokan_edit_product_url( $product_id ),
'class' => 'edit',
];
}
--- a/dokan-lite/includes/ProductEditor/FormSchema.php
+++ b/dokan-lite/includes/ProductEditor/FormSchema.php
@@ -407,6 +407,9 @@
$can_create_tags = dokan()->is_pro_exists() ? dokan_get_option( 'product_vendors_can_create_tags', 'dokan_selling', 'off' ) : 'off';
+ // Vendors may be restricted to a single product category via the admin selling settings.
+ $is_single_category = ProductCategoryHelper::product_category_selection_is_single();
+
$dep_downloadable = [
[
'comparison' => '==',
@@ -453,16 +456,18 @@
'label' => __( 'General', 'dokan-lite' ),
'required' => true,
'visibility' => true,
+ 'is_mandatory' => true,
],
[
'id' => Elements::NAME,
- 'section_id' => Elements::SECTION_GENERAL,
+ 'section_id' => Elements::SECTION_GENERAL,
'type' => 'field',
'label' => __( 'Title', 'dokan-lite' ),
'variant' => 'text',
'placeholder' => __( 'Enter product title...', 'dokan-lite' ),
'required' => true,
'visibility' => true,
+ 'is_mandatory' => true,
],
[
'id' => Elements::SLUG,
@@ -483,6 +488,7 @@
'variant' => 'select',
'value' => 'simple',
'required' => true,
+ 'is_mandatory' => true,
'options' => $this->get_product_types(),
'description' => __( 'Choose Variable if your product has multiple attributes - like sizes, colors, quality etc', 'dokan-lite' ),
'tooltip' => __( 'Choose product type.', 'dokan-lite' ),
@@ -579,11 +585,17 @@
'section_id' => Elements::SECTION_GENERAL,
'type' => 'field',
'label' => __( 'Categories', 'dokan-lite' ),
- 'variant' => 'multiselect',
+ 'variant' => 'async_select',
'placeholder' => __( 'Select product categories', 'dokan-lite' ),
'value' => [],
- 'options' => ProductCategoryHelper::get_product_categories_tree( true ),
+ // Loaded on demand as a nested tree instead of embedding the whole
+ // category hierarchy in the schema, which bloats memory on large catalogs.
+ 'api_endpoint' => '/dokan/v1/products/categories/tree',
+ 'tree' => true,
+ // Honor the admin "single vs. multiple" category selection setting.
+ 'multiple' => ! $is_single_category,
'required' => true,
+ 'is_mandatory' => true,
'visibility' => true,
],
[
@@ -591,10 +603,12 @@
'section_id' => Elements::SECTION_GENERAL,
'type' => 'field',
'label' => __( 'Tags', 'dokan-lite' ),
- 'variant' => 'multiselect',
+ 'variant' => 'async_select',
'placeholder' => 'on' === $can_create_tags ? __( 'Select tags/Add tags', 'dokan-lite' ) : __( 'Select product tags', 'dokan-lite' ),
'value' => [],
- 'options' => self::get_product_tags(),
+ // Tags load on demand from WooCommerce core (searchable/paginated) instead of
+ // embedding the whole tag taxonomy in the schema, which can exhaust memory on large stores.
+ 'api_endpoint' => '/wc/v3/products/tags',
'creatable' => 'on' === $can_create_tags,
'visibility' => true,
],
@@ -646,6 +660,7 @@
'variant' => 'editor',
'placeholder' => __( 'Enter product description', 'dokan-lite' ),
'required' => true,
+ 'is_mandatory' => true,
'visibility' => true,
],
[
@@ -862,6 +877,7 @@
'variant' => 'select',
'options' => dokan_get_product_visibility_options(),
'required' => true,
+ 'is_mandatory' => true,
'visibility' => true,
],
[
@@ -910,23 +926,55 @@
$items = apply_filters( 'dokan_product_editor_prepared_schema', $items, $product_id );
if ( $product instanceof WC_Product ) {
+ $values = $this->get_field_values( $items, $product );
foreach ( $items as &$item ) {
- if ( $item['type'] === 'field' ) {
- $value = $this->resolve_field_value( $item['id'], $product );
- $value = $this->format_field_value( $value, $item['variant'] ?? 'text' );
- if ( empty( $value ) && isset( $item['value'] ) ) {
- // set default value from schema if resolved value is empty, e.g. for new products or when product meta is not set.
- $value = $item['value'];
- }
- $item['value'] = $value;
+ if ( $item['type'] === 'field' && array_key_exists( $item['id'], $values ) ) {
+ $item['value'] = $values[ $item['id'] ];
}
}
+ unset( $item );
}
return $items;
}
/**
+ * Resolve and format values for a set of schema fields against a product.
+ *
+ * Lets callers that already hold field definitions resolve per-product
+ * values without rebuilding the whole schema. The frontend variation
+ * renderer uses this to avoid one full schema build per variation.
+ *
+ * @since 5.0.5
+ *
+ * @param array $fields Schema field items (each with at least 'id', 'type', 'variant').
+ * @param WC_Product $product Product to resolve values against.
+ *
+ * @return array<string, mixed> Map of field id => formatted value.
+ */
+ public function get_field_values( array $fields, WC_Product $product ): array {
+ $values = [];
+
+ foreach ( $fields as $field ) {
+ if ( ( $field['type'] ?? '' ) !== 'field' || ! isset( $field['id'] ) ) {
+ continue;
+ }
+
+ $value = $this->resolve_field_value( $field['id'], $product );
+ $value = $this->format_field_value( $value, $field['variant'] ?? 'text' );
+
+ if ( empty( $value ) && isset( $field['value'] ) ) {
+ // Fall back to the field's schema default, e.g. for new products or unset meta.
+ $value = $field['value'];
+ }
+
+ $values[ $field['id'] ] = $value;
+ }
+
+ return $values;
+ }
+
+ /**
* Format a resolved field value to the shape expected by the frontend based on variant.
*
* Resolve_field_value() returns raw values (int, array of ints, etc.).
@@ -1029,8 +1077,21 @@
case Elements::DATE_ON_SALE_TO:
$to = $product->get_date_on_sale_to( 'edit' );
return $to ? $to->date( 'Y-m-d' ) : '';
+ case Elements::CATEGORIES:
+ // Async select expects [ { value, label }, ... ] so selected categories render
+ // without the full category tree being embedded in the schema.
+ $category_options = self::terms_to_async_options( $product->get_category_ids(), 'product_cat' );
+
+ // Single-category stores keep one selection; surface only the first saved term.
+ if ( ProductCategoryHelper::product_category_selection_is_single() ) {
+ return array_slice( $category_options, 0, 1 );
+ }
+
+ return $category_options;
case Elements::TAGS:
- return $product->get_tag_ids();
+ // Async select expects [ { value, label }, ... ] so selected tags render
+ // without the full tag list being embedded in the schema.
+ return self::terms_to_async_options( $product->get_tag_ids(), 'product_tag' );
case Elements::BRANDS:
if ( method_exists( $product, 'get_brand_ids' ) ) {
return $product->get_brand_ids();
@@ -1121,6 +1182,50 @@
}
/**
+ * Convert a list of term IDs to async-select options: [ { value, label }, ... ].
+ *
+ * Used by async-select fields (e.g. tags) so the currently selected terms render
+ * their labels without embedding the whole taxonomy in the form schema.
+ *
+ * @since 5.0.5
+ *
+ * @param array $term_ids Term IDs.
+ * @param string $taxonomy Taxonomy name.
+ *
+ * @return array
+ */
+ public static function terms_to_async_options( array $term_ids, string $taxonomy ): array {
+ $term_ids = array_filter( array_map( 'absint', $term_ids ) );
+ if ( empty( $term_ids ) ) {
+ return [];
+ }
+
+ $terms = get_terms(
+ [
+ 'taxonomy' => $taxonomy,
+ 'include' => $term_ids,
+ 'hide_empty' => false,
+ 'orderby' => 'name',
+ 'order' => 'ASC',
+ ]
+ );
+
+ if ( is_wp_error( $terms ) ) {
+ return [];
+ }
+
+ return array_map(
+ function ( $term ) {
+ return [
+ 'value' => $term->term_id,
+ 'label' => $term->name,
+ ];
+ },
+ $terms
+ );
+ }
+
+ /**
* Get product brands recursively for form options.
*
* @since 5.0.0
--- a/dokan-lite/includes/ProductEditor/PayloadResolver.php
+++ b/dokan-lite/includes/ProductEditor/PayloadResolver.php
@@ -150,6 +150,12 @@
$result = [];
foreach ( $tags as $tag ) {
+ // Async-select sends option objects: [ { value, label, __isNew__ }, ... ].
+ // Existing tags carry a numeric term ID as value; newly created ones carry the typed name.
+ if ( is_array( $tag ) && isset( $tag['value'] ) ) {
+ $tag = $tag['value'];
+ }
+
if ( is_numeric( $tag ) && (int) $tag > 0 ) {
$result[] = [ 'id' => (int) $tag ];
continue;
@@ -340,6 +346,10 @@
public function map_ids_to_objects( array $ids ): array {
return array_map(
static function ( $id ) {
+ // Async-select sends option objects [ { value, label }, ... ]; pull the id out.
+ if ( is_array( $id ) && isset( $id['value'] ) ) {
+ $id = $id['value'];
+ }
return [ 'id' => (int) $id ];
},
$ids
--- a/dokan-lite/includes/REST/ProductAttributeController.php
+++ b/dokan-lite/includes/REST/ProductAttributeController.php
@@ -63,6 +63,57 @@
),
)
);
+
+ // 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',
+ ),
+ ),
+ ),
+ )
+ );
}
/**
@@ -129,6 +180,30 @@
}
/**
+ * 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.
@@ -271,4 +346,131 @@
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 ) );
+ }
}
--- a/dokan-lite/includes/REST/ProductController.php
+++ b/dokan-lite/includes/REST/ProductController.php
@@ -489,11 +489,31 @@
* Get_single_product_permissions_check
*
* @since 2.8.0
+ * @since 5.0.5 Added check for dokan_is_product_author()
*
* @return bool
*/
- public function get_single_product_permissions_check() {
- return current_user_can( 'dokandar' ) || current_user_can( 'manage_options' );
+ public function get_single_product_permissions_check( $request ) {
+ // Store admins may read any product.
+ if ( current_user_can( 'manage_options' ) ) {
+ return true;
+ }
+
+ if ( ! current_user_can( 'dokandar' ) ) {
+ return false;
+ }
+
+ // A vendor may only read their own product.
+ $request_id = (int) ( $request['id'] ?? 0 );
+ if ( ! dokan_is_product_author( $request_id ) ) {
+ return new WP_Error(
+ 'dokan_rest_cannot_view',
+ __( 'Sorry, you are not allowed to view this product.', 'dokan-lite' ),
+ [ 'status' => rest_authorization_required_code() ]
+ );
+ }
+
+ return true;
}
/**
--- a/dokan-lite/includes/REST/ProductControllerV2.php
+++ b/dokan-lite/includes/REST/ProductControllerV2.php
@@ -274,7 +274,7 @@
$product_types = apply_filters( 'dokan_product_types', [ 'simple' => __( 'Simple', 'dokan-lite' ) ] );
// If any vendor it trying to access other products then we need to replace author id by current user id.
- if ( ! current_user_can( dokana_admin_menu_capability() ) ) {
+ if ( ! current_user_can( dokan_admin_menu_capability() ) ) {
$args['author'] = dokan_get_current_user_id();
} elseif ( $request->get_param( 'author' ) ) {
$args['author'] = $request->get_param( 'author' );
--- a/dokan-lite/includes/REST/ReverseWithdrawalController.php
+++ b/dokan-lite/includes/REST/ReverseWithdrawalController.php
@@ -233,7 +233,7 @@
$manager = new Manager();
$items = [];
- if ( ! current_user_can( dokana_admin_menu_capability() ) || ! isset( $request_params['vendor_id'] ) ) {
+ if ( ! current_user_can( dokan_admin_menu_capability() ) || ! isset( $request_params['vendor_id'] ) ) {
$request_params['vendor_id'] = dokan_get_current_user_id();
}
@@ -293,7 +293,7 @@
public function get_vendor_due_status( $request ) {
$request_params = $request->get_params();
- if ( ! current_user_can( dokana_admin_menu_capability() ) || ! isset( $request_params['vendor_id'] ) ) {
+ if ( ! current_user_can( dokan_admin_menu_capability() ) || ! isset( $request_params['vendor_id'] ) ) {
$request_params['vendor_id'] = dokan_get_current_user_id();
}
@@ -498,7 +498,7 @@
* @return WP_REST_Response
*/
public function prepare_transaction_for_response( $item, $request, &$current_balance ) {
- $context = current_user_can( dokana_admin_menu_capability() ) ? 'admin' : 'seller';
+ $context = current_user_can( dokan_admin_menu_capability() ) ? 'admin' : 'seller';
$data = Helper::get_formated_transaction_data( $item, $current_balance, $context );
// Decode HTML entities in URL (wp_nonce_url encodes & as & which breaks in JSON/React context).
--- a/dokan-lite/includes/REST/VendorProductCategoriesController.php
+++ b/dokan-lite/includes/REST/VendorProductCategoriesController.php
@@ -7,6 +7,7 @@
use WP_REST_Response;
use WC_REST_Product_Categories_Controller;
use WP_REST_Server;
+use WeDevsDokanProductCategoryHelper as ProductCategoryHelper;
class VendorProductCategoriesController extends WC_REST_Product_Categories_Controller {
/**
@@ -35,6 +36,19 @@
)
);
+ // Nested category tree for the product editor's hierarchical category picker.
+ register_rest_route(
+ $this->namespace,
+ '/' . $this->rest_base . '/tree',
+ array(
+ array(
+ 'methods' => WP_REST_Server::READABLE,
+ 'callback' => array( $this, 'get_tree' ),
+ 'permission_callback' => array( $this, 'get_items_permissions_check' ),
+ ),
+ )
+ );
+
register_rest_route(
$this->namespace,
'/' . $this->rest_base . '/(?P<id>[d]+)',
@@ -104,4 +118,26 @@
return parent::get_item( $request );
}
+ /**
+ * Get the full nested product category tree.
+ *
+ * Returns the hierarchy as [ { value, label, children: [...] }, ... ] so the
+ * product editor can render an indented category picker without embedding the
+ * tree in the form schema.
+ *
+ * @since 5.0.5
+ *
+ * @param WP_REST_Request $request Full details about the request.
+ *
+ * @return WP_REST_Response
+ */
+ public function get_tree( $request ) {
+ $tree = apply_filters(
+ 'dokan_rest_product_categories_tree',
+ ProductCategoryHelper::get_product_categories_tree( true ),
+ $request
+ );
+
+ return rest_ensure_response( $tree );
+ }
}
--- a/dokan-lite/includes/functions-dashboard-navigation.php
+++ b/dokan-lite/includes/functions-dashboard-navigation.php
@@ -2,6 +2,7 @@
use WeDevsDokanUtilitiesReportUtil;
use WeDevsDokanDashboardTemplatesDashboard;
+use WeDevsDokanAdminDashboardLegacySwitcher;
/**
* Sort navigation menu items by position
@@ -29,6 +30,10 @@
* @return array
*/
function dokan_get_dashboard_nav(): array {
+ // The "Vendor Product Editor" setting drives the whole product UI, so the legacy editor also keeps the legacy listing page.
+ // TODO: Drop this toggle once the legacy product pages are removed — then always register the new (React) product page.
+ $use_new_product_ui = ! dokan_get_container()->get( LegacySwitcher::class )->is_product_editor_legacy_preferred();
+
$menus = [
'dashboard' => [
'title' => __( 'Dashboard', 'dokan-lite' ),
@@ -41,11 +46,10 @@
'products' => [
'title' => __( 'Products', 'dokan-lite' ),
'icon' => '<i class="fas fa-briefcase"></i>',
- 'url' => dokan_get_navigation_url( 'products' ),
+ 'url' => dokan_get_navigation_url( 'products', $use_new_product_ui ),
'pos' => 30,
'icon_name' => 'Box',
'permission' => 'dokan_view_product_menu',
- 'react_route' => 'products',
],
'orders' => [
'title' => __( 'Orders', 'dokan-lite' ),
--- a/dokan-lite/includes/functions.php
+++ b/dokan-lite/includes/functions.php
@@ -19,9 +19,13 @@
*
* @since 3.0.0
*
+ * @deprecated 5.0.5 Misspelled name; use dokan_admin_menu_capability() instead.
+ *
* @return string
*/
function dokana_admin_menu_capability() {
+ wc_deprecated_function( 'dokana_admin_menu_capability', '5.0.5', 'dokan_admin_menu_capability()' );
+
return dokan_admin_menu_capability();
}
--- a/dokan-lite/templates/whats-new.php
+++ b/dokan-lite/templates/whats-new.php
@@ -4,6 +4,44 @@
*/
$changelog = [
[
+ 'version' => 'Version 5.0.5',
+ 'released' => '2026-06-19',
+ 'changes' => [
+ 'Improvement' => [
+ [
+ 'title' => 'Migrated all admin dashboard tables to the unified Plugin UI DataViews component.',
+ 'description' => '',
+ ],
+ [
+ 'title' => 'Updated price formatting in the product editor to use locale-specific display.',
+ 'description' => '',
+ ],
+ ],
+ 'Fix' => [
+ [
+ 'title' => 'Lazy-loaded product editor taxonomies (attributes, categories, and tags) to prevent memory exhaustion on large catalogues.',
+ 'description' => '',
+ ],
+ [
+ 'title' => 'Escaped vendor-controlled values in store product search results to prevent stored XSS via a product SKU.',
+ 'description' => '',
+ ],
+ [
+ 'title' => 'Restricted the Products REST endpoint so vendors can no longer access other vendors' products via the id parameter.',
+ 'description' => '',
+ ],
+ [
+ 'title' => 'Decoded HTML entities in product category labels for correct rendering.',
+ 'description' => '',
+ ],
+ [
+ 'title' => 'Corrected the product edit URL and improved dashboard navigation for the new product UI.',
+ 'description' => '',
+ ],
+ ],
+ ],
+ ],
+ [
'version' => 'Version 5.0.4',
'released' => '2026-06-08',
'changes' => [
--- a/dokan-lite/vendor/composer/installed.php
+++ b/dokan-lite/vendor/composer/installed.php
@@ -1,9 +1,9 @@
<?php return array(
'root' => array(
'name' => 'wedevs/dokan',
- 'pretty_version' => 'v5.0.4',
- 'version' => '5.0.4.0',
- 'reference' => 'b41469e02ac35d3fd21f81c4ecee91f789478df1',
+ 'pretty_version' => 'v5.0.5',
+ 'version' => '5.0.5.0',
+ 'reference' => '247986f9d91343515908c75f952ceb81d1753008',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
@@ -38,9 +38,9 @@
'dev_requirement' => false,
),
'wedevs/dokan' => array(
- 'pretty_version' => 'v5.0.4',
- 'version' => '5.0.4.0',
- 'reference' => 'b41469e02ac35d3fd21f81c4ecee91f789478df1',
+ 'pretty_version' => 'v5.0.5',
+ 'version' => '5.0.5.0',
+ 'reference' => '247986f9d91343515908c75f952ceb81d1753008',
'type' => 'wordpress-plugin',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),