Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4645f82
Fix profile links not showing for vendor staff
midu-01 Dec 22, 2025
5ace902
Fix Social Profile Links Not Showing for Vendor Staff Members
midu-01 Dec 22, 2025
f314a1f
Fix store update permission using dokan_is_user_seller and resolve st…
midu-01 Dec 23, 2025
5d79aea
Refactor store access: add check_vendor_authorizable_permission and r…
midu-01 Dec 25, 2025
33dbd5b
Fix PHPCS error
midu-01 Dec 26, 2025
36978fc
Fix PHPCS error
midu-01 Dec 26, 2025
ed15346
Fix PHPCS error
midu-01 Dec 26, 2025
5f60de5
Fix vendor ID resolution for vendor and staff users
midu-01 Dec 26, 2025
ef0aef1
Fix vendor ID resolution for vendor and staff users
midu-01 Dec 26, 2025
22970a2
Fix vendor ID resolution for vendor and staff users
midu-01 Dec 26, 2025
c629f2e
Fix vendor ID resolution for vendor and staff users
midu-01 Dec 26, 2025
d01af02
Fix vendor ID resolution for vendor and staff users
midu-01 Dec 26, 2025
408f819
Fix vendor ID resolution for vendor and staff users
midu-01 Dec 26, 2025
8b04408
Fix vendor ID resolution for vendor and staff users
midu-01 Dec 26, 2025
eaf5262
Fix vendor staff store access and improve authorization logic
midu-01 Dec 29, 2025
b4b3423
Merge branch 'develop' into fix/profile-links-not-showing-for-vendor-…
mrabbani Jan 1, 2026
74b4c91
feat: Add authorization tests and refactor store validation to use Ve…
mrabbani Jan 1, 2026
48b559f
fix: improve permission handling and vendor ID resolution
mrabbani Jan 1, 2026
29fa6fc
feat: make get_store endpoint truly public with data filtering
mrabbani Jan 1, 2026
1dc4afb
feat: add field restrictions for vendor staff and refactor StoreSetti…
mrabbani Jan 2, 2026
53a76af
Add util method create_vendor_staff
mrabbani Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 175 additions & 20 deletions includes/REST/StoreController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace WeDevs\Dokan\REST;

use WeDevs\Dokan\Traits\VendorAuthorizable;
use WeDevs\Dokan\Vendor\Vendor;
use WP_Error;
use WP_Query;
Expand All @@ -18,6 +19,7 @@
* @author weDevs <[email protected]>
*/
class StoreController extends WP_REST_Controller {
use VendorAuthorizable;

/**
* Endpoint namespace
Expand Down Expand Up @@ -62,7 +64,7 @@ public function register_routes() {
'description' => __( 'Unique identifier for the object.', 'dokan-lite' ),
'type' => 'integer',
'sanitize_callback' => 'absint',
'validate_callback' => 'dokan_rest_validate_store_id',
'validate_callback' => [ $this, 'validate_store_id' ],
],
],
[
Expand Down Expand Up @@ -304,16 +306,30 @@ public function get_stores( $request ) {
/**
* Get singe store
*
* Public endpoint: Returns public data for all users/guests (respecting admin settings).
* Sensitive data is only returned for authorized users (vendor, vendor staff, or admin).
*
* For vendor staff accessing via their own ID, the vendor ID is resolved to show their vendor's store.
* Vendors and vendor staff attempting to access another vendor's store will be blocked (403).
*
* @since 1.0.0
*
* @param $request
*
* @return WP_Error|WP_REST_Response
*/
public function get_store( $request ) {
$store_id = (int) $request['id'];
$requested_id = absint( $request->get_param( 'id' ) );

$store = dokan()->vendor->get( $store_id );
$store = dokan()->vendor->get( $requested_id );

if ( ! $store || ! $store->get_id() ) {
return new WP_Error(
'dokan_rest_store_not_found',
__( 'Store not found.', 'dokan-lite' ),
[ 'status' => 404 ]
);
}

$stores_data = $this->prepare_item_for_response( $store, $request );
$response = rest_ensure_response( $stores_data );
Expand Down Expand Up @@ -357,13 +373,12 @@ public function delete_store( $request ) {
* @return bool
*/
public function update_store_permissions_check( $request ) {
if ( current_user_can( 'manage_woocommerce' ) ) {
return true;
}
$requested_id = absint( $request->get_param( 'id' ) );

if ( current_user_can( 'dokandar' ) ) {
return dokan_get_current_user_id() === absint( $request->get_param( 'id' ) );
}
// Resolve vendor ID: handles both vendor IDs and vendor staff IDs
$store_id = $this->get_vendor_id_for_user( $requested_id );

return $this->can_access_vendor_store( $store_id );
}

/**
Expand All @@ -376,17 +391,54 @@ public function update_store_permissions_check( $request ) {
* @return WP_Error|WP_REST_Response
*/
public function update_store( $request ) {
$store = dokan()->vendor->get( (int) $request->get_param( 'id' ) );
$requested_id = absint( $request->get_param( 'id' ) );

$params = $request->get_params();
$store_id = dokan()->vendor->update( $store->get_id(), $params );
// Resolve vendor ID: handles both vendor IDs and vendor staff IDs
$store_id = $this->get_vendor_id_for_user( $requested_id );

if ( is_wp_error( $store_id ) ) {
return new WP_Error( $store_id->get_error_code(), $store_id->get_error_message() );
if ( ! $store_id ) {
return new WP_Error(
'dokan_rest_store_not_found',
__( 'Store not found.', 'dokan-lite' ),
[ 'status' => 404 ]
);
}

$store = dokan()->vendor->get( $store_id );

if ( ! $store || ! $store->get_id() ) {
return new WP_Error(
'dokan_rest_store_not_found',
__( 'Store not found.', 'dokan-lite' ),
[ 'status' => 404 ]
);
}
if ( ! $this->can_access_vendor_store( $store->get_id() ) ) {
return new WP_Error(
'dokan_rest_store_cannot_access',
__( 'You do not have permission to access this store.', 'dokan-lite' ),
[ 'status' => 403 ]
);
}

$params = $request->get_params();

$restricted_fields = $this->get_restricted_fields_for_update( $store, $request );

foreach ( $restricted_fields as $field ) {
if ( isset( $params[ $field ] ) ) {
unset( $params[ $field ] );
}
}

$updated_store_id = dokan()->vendor->update( $store->get_id(), $params );

if ( is_wp_error( $updated_store_id ) ) {
return new WP_Error( $updated_store_id->get_error_code(), $updated_store_id->get_error_message() );
}

$store = dokan()->vendor->get( $updated_store_id );

do_action( 'dokan_rest_stores_update_store', $store, $request );

$stores_data = $this->prepare_item_for_response( $store, $request );
Expand All @@ -395,6 +447,43 @@ public function update_store( $request ) {
return $response;
}

/**
* Get restricted fields for store update based on user role.
*
* @since DOKAN_SINCE
*
* @param \WeDevs\Dokan\Vendor\Vendor $store Store object.
* @param \WP_REST_Request $request Request object.
*
* @return array Array of restricted field names.
*/
protected function get_restricted_fields_for_update( $store, $request ) {
$is_admin = current_user_can( 'manage_options' );
$is_vendor = dokan_is_user_seller( get_current_user_id(), true );
$restricted_fields = [];

if ( ! $is_admin && ! $is_vendor ) {
$staff_restricted_fields = [
'email',
'password',
];
array_push( $restricted_fields, ...$staff_restricted_fields );
}

if ( ! $is_admin ) {
$vendor_restricted_fields = [
'dokan_admin_percentage',
'dokan_admin_percentage_type',
'dokan_admin_additional_fee',
'admin_category_commission',
];

array_push( $restricted_fields, ...$vendor_restricted_fields );
}

return apply_filters( 'dokan_rest_store_restricted_fields_for_update', $restricted_fields, $store, $request );
}

/**
* Create store
*
Expand Down Expand Up @@ -623,28 +712,94 @@ public function get_total_review_count( $id, $post_type, $status ) {
/**
* Prepare a single user output for response
*
* Public data is returned for all users/guests (respecting admin settings for hiding vendor info).
* Sensitive data is only returned for authorized users (vendor, vendor staff, or admin).
*
* @param Vendor $store
* @param WP_REST_Request $request Request object.
* @param array $additional_fields (optional)
* @param bool $is_authorized (optional) Whether the current user is authorized to view sensitive data.
*
* @return WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $store, $request, $additional_fields = [] ) {
$data = $store->to_array();

$commission_settings = $store->get_commission_settings();
$data['admin_category_commission'] = $commission_settings->get_category_commissions();
$data['admin_commission'] = $commission_settings->get_percentage();
$data['admin_additional_fee'] = $commission_settings->get_flat();
$data['admin_commission_type'] = $commission_settings->get_type();
$is_authorized = $this->can_access_vendor_store( $store->get_id() );

if ( $is_authorized ) {
$data['admin_category_commission'] = $store->get_commission_settings()->get_category_commissions();
$data['admin_commission'] = $store->get_commission_settings()->get_percentage();
$data['admin_additional_fee'] = $store->get_commission_settings()->get_flat();
$data['admin_commission_type'] = $store->get_commission_settings()->get_type();
}

$restricted_fields = $this->get_restricted_fields_for_view( $store, $request );

foreach ( $restricted_fields as $field ) {
unset( $data[ $field ] );
}

$data = array_merge( $data, apply_filters( 'dokan_rest_store_additional_fields', $additional_fields, $store, $request ) );
$data = array_merge( $data, apply_filters( 'dokan_rest_store_additional_fields', $additional_fields, $store, $request, $is_authorized ) );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $data, $request ) );

return apply_filters( 'dokan_rest_prepare_store_item_for_response', $response );
}

/**
* Get restricted fields for store view based on user authorization.
*
* Determines which fields should be hidden from the store data response based on:
* - User authorization status (authorized users see more data)
* - User role (vendor staff cannot see admin commission data)
* - Admin settings (for hiding vendor info like address, phone, email)
* - Vendor preferences (vendor can choose to hide email)
*
* @since DOKAN_SINCE
*
* @param \WeDevs\Dokan\Vendor\Vendor $store Store object.
* @param \WP_REST_Request $request Request object.
*
* @return array Array of restricted field names that should be removed from the response.
*/
protected function get_restricted_fields_for_view( $store, $request ) {
$restricted_fields = [];

$is_authorized = $this->can_access_vendor_store( $store->get_id() );

// Restrict admin commission fields for unauthorized users and vendor staff
if ( ! $is_authorized || ( $is_authorized && $this->is_staff_only( get_current_user_id() ) ) ) {
$restricted_fields[] = 'admin_category_commission';
$restricted_fields[] = 'admin_commission';
$restricted_fields[] = 'admin_additional_fee';
$restricted_fields[] = 'admin_commission_type';
}

// Additional restrictions for unauthorized users (public access)
if ( ! $is_authorized ) {
// Respect admin settings for hiding vendor info
if ( dokan_is_vendor_info_hidden( 'address' ) ) {
$restricted_fields[] = 'address';
}

if ( dokan_is_vendor_info_hidden( 'phone' ) ) {
$restricted_fields[] = 'phone';
}

// Hide email if admin setting hides it OR vendor doesn't want to show it
if ( dokan_is_vendor_info_hidden( 'email' ) || ! $store->show_email() ) {
$restricted_fields[] = 'email';
}

// Always hide sensitive payment and store status data from public
$restricted_fields[] = 'payment';
$restricted_fields[] = 'enabled';
}

return apply_filters( 'dokan_rest_store_restricted_fields_for_view', $restricted_fields, $store, $request );
}

/**
* Prepare a single user output for response
*
Expand Down
39 changes: 13 additions & 26 deletions includes/REST/StoreSettingController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,11 @@

/**
* StoreSettings API Controller
*
* @package dokan
*
* @author weDevs <[email protected]>
*/
class StoreSettingController extends WP_REST_Controller {
class StoreSettingController extends StoreController {
/**
* Endpoint namespace
*
Expand Down Expand Up @@ -83,44 +82,31 @@
* @return WP_Error|\WP_REST_Response
*/
public function update_settings( $request ) {
$vendor = $this->get_vendor( $request );
$params = $request->get_params();
$store_id = dokan()->vendor->update( $vendor->get_id(), $params );
$vendor_id = (int) $request->get_param( 'vendor_id' );
$request->set_param( 'id', $vendor_id );
$response = parent::update_store( $request );

if ( is_wp_error( $store_id ) ) {
return new WP_Error( $store_id->get_error_code(), $store_id->get_error_message() );
if ( is_wp_error( $response ) ) {
return $response;
}

$store = dokan()->vendor->get( $store_id );
$store = dokan()->vendor->get( $vendor_id );

do_action( 'dokan_rest_store_settings_after_update', $store, $request );

$stores_data = $this->prepare_item_for_response( $store, $request );
$response = rest_ensure_response( $stores_data );

return $response;
}
Comment on lines 84 to 98
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Edge case: vendor_id = 0 when parameter is not provided.

When vendor_id is not in the request, (int) $request->get_param( 'vendor_id' ) returns 0. This 0 is then set as the id parameter, which will cause parent::update_store() to fail with a 404 error since store ID 0 is invalid.

The get_vendor method handles missing vendor_id by falling back to the current user, but update_settings doesn't use that fallback. Consider aligning the behavior.

🔎 Proposed fix
     public function update_settings( $request ) {
         $vendor_id = (int) $request->get_param( 'vendor_id' );
+
+        // Fall back to current user's vendor ID if not provided
+        if ( ! $vendor_id ) {
+            $vendor = $this->get_vendor( $request );
+            if ( ! is_wp_error( $vendor ) && $vendor->get_id() ) {
+                $vendor_id = $vendor->get_id();
+            }
+        }
+
         $request->set_param( 'id', $vendor_id );
         $response = parent::update_store( $request );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function update_settings( $request ) {
$vendor = $this->get_vendor( $request );
$params = $request->get_params();
$store_id = dokan()->vendor->update( $vendor->get_id(), $params );
$vendor_id = (int) $request->get_param( 'vendor_id' );
$request->set_param( 'id', $vendor_id );
$response = parent::update_store( $request );
if ( is_wp_error( $store_id ) ) {
return new WP_Error( $store_id->get_error_code(), $store_id->get_error_message() );
if ( is_wp_error( $response ) ) {
return $response;
}
$store = dokan()->vendor->get( $store_id );
$store = dokan()->vendor->get( $vendor_id );
do_action( 'dokan_rest_store_settings_after_update', $store, $request );
$stores_data = $this->prepare_item_for_response( $store, $request );
$response = rest_ensure_response( $stores_data );
return $response;
}
public function update_settings( $request ) {
$vendor_id = (int) $request->get_param( 'vendor_id' );
// Fall back to current user's vendor ID if not provided
if ( ! $vendor_id ) {
$vendor = $this->get_vendor( $request );
if ( ! is_wp_error( $vendor ) && $vendor->get_id() ) {
$vendor_id = $vendor->get_id();
}
}
$request->set_param( 'id', $vendor_id );
$response = parent::update_store( $request );
if ( is_wp_error( $response ) ) {
return $response;
}
$store = dokan()->vendor->get( $vendor_id );
do_action( 'dokan_rest_store_settings_after_update', $store, $request );
return $response;
}


/**
* @param $request
* @param \WP_REST_Request $request
*
* @return mixed|WP_Error|\WP_HTTP_Response|\WP_REST_Response
*/
public function get_settings( $request ) {
$vendor = $this->get_vendor( $request );
$response = dokan_get_store_info( $vendor->id );

$methods_data = dokan_get_container()->get( 'dashboard' )->templates->settings->get_seller_payment_methods( $vendor->get_id() );

$response['bank_payment_required_fields'] = dokan_bank_payment_required_fields();
$response['active_payment_methods'] = $methods_data['active_methods'] ?? [];
$response['connected_methods'] = $methods_data['connected_methods'] ?? [];
$response['disconnected_methods'] = $methods_data['disconnected_methods'] ?? [];
$response['withdraw_options'] = dokan_withdraw_get_methods();
$response['fields_placeholders'] = dokan_bank_payment_fields_placeholders();
$response['chargeable_methods'] = dokan_withdraw_get_chargeable_methods();
$vendor_id = (int) $request->get_param( 'vendor_id' );
$request->set_param( 'id', $vendor_id );

return rest_ensure_response( $response );
return parent::get_store( $request );
}
Comment on lines 105 to 110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same edge case in get_settings: missing vendor_id defaults to 0.

Similar to update_settings, when vendor_id is not provided, the code sets id to 0, which will fail in parent::get_store().

🔎 Proposed fix
     public function get_settings( $request ) {
         $vendor_id = (int) $request->get_param( 'vendor_id' );
+
+        // Fall back to current user's vendor ID if not provided
+        if ( ! $vendor_id ) {
+            $vendor = $this->get_vendor( $request );
+            if ( ! is_wp_error( $vendor ) && $vendor->get_id() ) {
+                $vendor_id = $vendor->get_id();
+            }
+        }
+
         $request->set_param( 'id', $vendor_id );

         return parent::get_store( $request );
     }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In includes/REST/StoreSettingController.php around lines 105 to 110,
get_settings currently casts a missing vendor_id to 0 and sets request id to 0
which causes parent::get_store() to fail; update the method to validate the
vendor_id before setting it (check that request has vendor_id and that it casts
to an int > 0), and if missing/invalid return an appropriate WP_Error or
WP_REST_Response with a 400/422 status; only call $request->set_param('id',
$vendor_id) and parent::get_store($request) when vendor_id is valid.


/**
Expand Down Expand Up @@ -176,7 +162,7 @@
*
* @return array Links for the given post.
*/
protected function prepare_links( $object, $request ) {

Check warning on line 165 in includes/REST/StoreSettingController.php

View workflow job for this annotation

GitHub Actions / Run PHPCS inspection

It is recommended not to use reserved keyword "object" as function parameter name. Found: $object
$links = [
'self' => [
'href' => rest_url( sprintf( '/%s/%s/%d', $this->namespace, $this->rest_base, $object['id'] ) ),
Expand All @@ -199,7 +185,8 @@
* @return \WP_REST_Response $response Response data.
*/
public function prepare_item_for_response( $store, $request, $additional_fields = [] ) {
$data = $store->to_array();
$response = parent::prepare_item_for_response( $store, $request, $additional_fields );
$data = $response->get_data();
$data = array_merge( $data, apply_filters( 'dokan_rest_store_settings_additional_fields', $additional_fields, $store, $request ) );
$response = rest_ensure_response( $data );
$response->add_links( $this->prepare_links( $data, $request ) );
Expand Down
Loading
Loading