Skip to content

Conversation

@kzamanbd
Copy link
Contributor

@kzamanbd kzamanbd commented Jan 26, 2026

All Submissions:

  • My code follow the WordPress' coding standards
  • My code satisfies feature requirements
  • My code is tested
  • My code passes the PHPCS tests
  • My code has proper inline documentation
  • I've included related pull request(s) (optional)
  • I've included developer documentation (optional)
  • I've added proper labels to this pull request

Changes proposed in this Pull Request:

Related Pull Request(s)

  • Full PR Link

Closes

How to test the changes in this Pull Request:

  • Steps or issue link

Changelog entry

Title

Detailed Description of the pull request. What was previous behaviour
and what will be changed in this PR.

Before Changes

Describe the issue before changes with screenshots(s).

After Changes

Describe the issue after changes with screenshot(s).

Feature Video (optional)

Link of detailed video if this PR is for a feature.

PR Self Review Checklist:

  • Code is not following code style guidelines
  • Bad naming: make sure you would understand your code if you read it a few months from now.
  • KISS: Keep it simple, Sweetie (not stupid!).
  • DRY: Don't Repeat Yourself.
  • Code that is not readable: too many nested 'if's are a bad sign.
  • Performance issues
  • Complicated constructions that need refactoring or comments: code should almost always be self-explanatory.
  • Grammar errors.

FOR PR REVIEWER ONLY:

As a reviewer, your feedback should be focused on the idea, not the person. Seek to understand, be respectful, and focus on constructive dialog.

As a contributor, your responsibility is to learn from suggestions and iterate your pull request should it be needed based on feedback. Seek to collaborate and produce the best possible contribution to the greater whole.

  • Correct — Does the change do what it’s supposed to? ie: code 100% fulfilling the requirements?
  • Secure — Would a nefarious party find some way to exploit this change? ie: everything is sanitized/escaped appropriately for any SQL or XSS injection possibilities?
  • Readable — Will your future self be able to understand this change months down the road?
  • Elegant — Does the change fit aesthetically within the overall style and architecture?

Summary by CodeRabbit

  • New Features

    • Extended order downloads REST API to accept optional remaining-downloads and access-expiry parameters when granting permissions.
  • Bug Fixes

    • Safer handling when products or download files are missing to prevent errors and improve stability.
  • Chores

    • Updated development configuration to ignore local editor settings (.vscode).

✏️ Tip: You can customize this high-level summary in your review settings.

Introduces two new REST API endpoints to grant and revoke downloadable product access for orders. Implements the corresponding controller methods to handle permission management, including input validation and response formatting.
@kzamanbd kzamanbd self-assigned this Jan 26, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

Adds a .vscode ignore entry and extends order download permission handling: new optional REST parameters (download_remaining, access_expires), reuse/update of existing downloadable permissions via WC_Data_Store, and safer product/file checks in V3 to avoid operating on missing products/files.

Changes

Cohort / File(s) Summary
Configuration
​.gitignore
Added .vscode to ignore rules.
Order Download Permissions API (V2)
includes/REST/OrderControllerV2.php
Added REST params download_remaining (int) and access_expires (string); added use WC_Customer_Download; refactored grant logic to check for/reuse existing permissions via WC_Data_Store, create/update WC_Customer_Download when needed, and apply provided remaining/download expiry values before saving.
Safe download assembly (V3)
includes/REST/OrderControllerV3.php
Guarded product assignment and wrapped product/file-dependent logic in an existence check; made file retrieval nullable and added instanceof checks to avoid operating on missing products/files.

Sequence Diagram(s)

sequenceDiagram
    participant Client as REST Client
    participant Controller as OrderControllerV2
    participant Store as WC_Data_Store
    participant Download as WC_Customer_Download

    Client->>Controller: POST grant_order_downloads(order_id, product_id, download_remaining?, access_expires?)
    Controller->>Store: Lookup existing permission for order/product/file
    alt permission exists
        Store-->>Controller: permission_id
        Controller->>Download: Load existing permission (permission_id)
        alt update params provided
            Controller->>Download: set downloads_remaining / set access_expires
            Download->>Store: save updated permission
            Store-->>Controller: updated_id
        end
    else permission not found
        Controller->>Download: create new WC_Customer_Download with file/order/product
        alt params provided
            Controller->>Download: set downloads_remaining / set access_expires
        end
        Download->>Store: insert new permission
        Store-->>Controller: new_permission_id
    end
    Controller->>Controller: increment file counter / assemble response
    Controller-->>Client: return grant result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

Needs: Testing, Dev Review Done

Suggested reviewers

  • mrabbani
  • MdAsifHossainNadim

Poem

🐰 I hopped through code with nimble feet,
Reused permissions, no duplicate treat.
A download here, an expiry there,
I saved the day with careful care—
Hooray for tidy flows and fewer repeat!

🚥 Pre-merge checks | ✅ 1 | ❌ 4
❌ Failed checks (2 warnings, 2 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains the standard template checklist with most items unchecked and critical sections left unfilled: 'Changes proposed' lacks details, testing steps are missing, changelog entry is empty, and before/after descriptions are absent. Complete the description by filling in the 'Changes proposed' section with implementation details, providing testing steps, adding a meaningful changelog entry, and including before/after context to explain the changes.
Out of Scope Changes check ⚠️ Warning The .gitignore change (adding .vscode) is unrelated to the downloadable permission REST API functionality outlined in the linked issue #1394, representing an out-of-scope modification. Remove the .gitignore change from this PR or move it to a separate, focused commit/PR dedicated to repository configuration updates, keeping this PR focused solely on downloadable permission REST API implementation.
Title check ❓ Inconclusive The title 'Feat/downloadable permission rest' is partially related to the changeset; it refers to downloadable permissions REST functionality, but is abbreviated and uses a vague 'rest' suffix that doesn't clearly convey the full scope. Improve title clarity by expanding 'Feat/downloadable permission rest' to something like 'Add downloadable product permission management REST endpoints' to better describe the actual implementation.
Linked Issues check ❓ Inconclusive The code changes add grant_order_downloads parameters (access_expires, download_remaining) and refactor download handling logic with null-safety, partially addressing issue #1394's requirements for granting and updating permissions, but lack evidence of implementing revoke permission functionality. Verify whether revoke permission endpoint implementation is included in the changeset; if not, clarify whether it is in a separate PR or if this PR only addresses grant/update functionality from issue #1394.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 87.50% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@includes/REST/ProductController.php`:
- Around line 1902-1919: The revoke_access_to_download method currently deletes
customer download rows blindly by permission_id; update
revoke_access_to_download to first load the customer-download record (via
WC_Data_Store::load('customer-download') and the record lookup for the given
permission_id), verify it exists and that its download_id, product_id and
order_id match the request parameters, and confirm the current user/vendor is
authorized to revoke that specific permission (e.g., matches vendor/store
ownership or has capability); if not found or not owned, return a WP_Error with
a 404/403 status instead of proceeding to delete, and only call delete_by_id and
do_action('woocommerce_ajax_revoke_access_to_product_download', ...) after these
checks.
- Around line 1860-1866: The current truthy checks prevent setting downloads
remaining to 0 or explicitly clearing expiry; modify the conditional tests
around $remaining and $expiry in the ProductController download update block so
they check for null (e.g. $remaining !== null and $expiry !== null) before
calling $download->set_downloads_remaining(...) and
$download->set_access_expires(...), ensuring zero values and explicit expiry
updates are applied while leaving other values untouched.
- Around line 1811-1834: The grant_downloadable_access handler must first ensure
the order returned by dokan()->order->get($order_id) is a valid object before
calling $order->get_billing_email(), and must verify ownership so a vendor
cannot grant access for other vendors' orders/products. Fix by: validate $order
is not null/false and return a proper REST error if missing; fetch the current
vendor ID (e.g. dokan_get_current_user_id()) and check that the order (or
sub‑order) belongs to that vendor and that each $product (from
$product->get_id() / $product post author) belongs to the same vendor before
proceeding to build $granted_files; if any ownership check fails, skip that
product or return a permission error. Implement these checks in
grant_downloadable_access around the existing dokan()->order->get(...) and
product loop.
- Around line 323-351: The REST arg definition for access_expires is using an
unsupported type 'date'; update the 'access_expires' entry in the register_route
args (the array passed to register_rest_route in ProductController) to use
'type' => 'string' and add 'format' => 'date-time' (and optionally keep
'required' => false) so it conforms to WP REST API valid types and date-time
formatting rules; locate the 'access_expires' key near the
grant-downloadable-access route callback (grant_downloadable_access) and replace
the 'date' type accordingly.

Comment on lines 323 to 351
$this->namespace, '/' . $this->base . '/grant-downloadable-access', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'grant_downloadable_access' ],
'permission_callback' => [ $this, 'get_product_permissions_check' ],
'args' => [
'order_id' => [
'description' => __( 'Order ID', 'dokan-lite' ),
'type' => 'integer',
'required' => true,
],
'product_ids' => [
'description' => __( 'Product IDs', 'dokan-lite' ),
'type' => 'array',
'items' => [
'type' => 'integer',
],
'required' => true,
],
'download_remaining' => [
'description' => __( 'Download Remaining', 'dokan-lite' ),
'type' => 'integer',
'required' => false,
],
'access_expires' => [
'description' => __( 'Access Expires', 'dokan-lite' ),
'type' => 'date',
'required' => false,
],
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

🧩 Analysis chain

🌐 Web query:

WordPress REST API register_rest_route argument schema type date datetime supported values

💡 Result:

In register_rest_route( ... 'args' => ... ), date/datetime are not valid JSON Schema type values in WordPress. For a date/time argument you should use:

'my_date' => [
  'type'   => 'string',
  'format' => 'date-time', // RFC3339
]

Supported type values (WP REST arg schema) are: array, object, string, number, integer, boolean, null. [3]

Supported string format values in WordPress are: date-time (RFC3339), uri, email, ip, uuid, hex-color. [1]


🏁 Script executed:

# First, let's locate and examine the file
find . -name "ProductController.php" -type f

Repository: getdokan/dokan

Length of output: 95


🏁 Script executed:

# Read the ProductController.php file around lines 323-351
cat -n includes/REST/ProductController.php | sed -n '320,355p'

Repository: getdokan/dokan

Length of output: 1980


Fix REST argument type for access_expires.

Line 349 uses 'type' => 'date', which is not a supported WordPress REST API argument type. Use 'type' => 'string' with 'format' => 'date-time' instead. Valid REST arg types are: array, object, string, number, integer, boolean, null. The current type will cause REST schema validation failures.

♻️ Proposed fix
 'access_expires' => [
     'description' => __( 'Access Expires', 'dokan-lite' ),
-    'type'        => 'date',
+    'type'        => 'string',
+    'format'      => 'date-time',
     'required'    => false,
 ],
📝 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
$this->namespace, '/' . $this->base . '/grant-downloadable-access', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'grant_downloadable_access' ],
'permission_callback' => [ $this, 'get_product_permissions_check' ],
'args' => [
'order_id' => [
'description' => __( 'Order ID', 'dokan-lite' ),
'type' => 'integer',
'required' => true,
],
'product_ids' => [
'description' => __( 'Product IDs', 'dokan-lite' ),
'type' => 'array',
'items' => [
'type' => 'integer',
],
'required' => true,
],
'download_remaining' => [
'description' => __( 'Download Remaining', 'dokan-lite' ),
'type' => 'integer',
'required' => false,
],
'access_expires' => [
'description' => __( 'Access Expires', 'dokan-lite' ),
'type' => 'date',
'required' => false,
],
$this->namespace, '/' . $this->base . '/grant-downloadable-access', [
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => [ $this, 'grant_downloadable_access' ],
'permission_callback' => [ $this, 'get_product_permissions_check' ],
'args' => [
'order_id' => [
'description' => __( 'Order ID', 'dokan-lite' ),
'type' => 'integer',
'required' => true,
],
'product_ids' => [
'description' => __( 'Product IDs', 'dokan-lite' ),
'type' => 'array',
'items' => [
'type' => 'integer',
],
'required' => true,
],
'download_remaining' => [
'description' => __( 'Download Remaining', 'dokan-lite' ),
'type' => 'integer',
'required' => false,
],
'access_expires' => [
'description' => __( 'Access Expires', 'dokan-lite' ),
'type' => 'string',
'format' => 'date-time',
'required' => false,
],
🤖 Prompt for AI Agents
In `@includes/REST/ProductController.php` around lines 323 - 351, The REST arg
definition for access_expires is using an unsupported type 'date'; update the
'access_expires' entry in the register_route args (the array passed to
register_rest_route in ProductController) to use 'type' => 'string' and add
'format' => 'date-time' (and optionally keep 'required' => false) so it conforms
to WP REST API valid types and date-time formatting rules; locate the
'access_expires' key near the grant-downloadable-access route callback
(grant_downloadable_access) and replace the 'date' type accordingly.

Comment on lines 1811 to 1834
public function grant_downloadable_access( $request ) {
$order_id = $request->get_param( 'order_id' );
$product_ids = $request->get_param( 'product_ids' );
$remaining = $request->get_param( 'download_remaining' );
$expiry = $request->get_param( 'access_expires' );

if ( ! is_array( $product_ids ) ) {
$product_ids = [ $product_ids ];
}

$order = dokan()->order->get( $order_id );
$granted_files = [];

foreach ( $product_ids as $product_id ) {
$product = wc_get_product( $product_id );
if ( ! $product ) {
continue;
}
$files = $product->get_downloads();

if ( ! $order->get_billing_email() ) {
continue;
}

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 | 🟠 Major

Validate order existence and vendor/order ownership before granting access.

dokan()->order->get() can return null/invalid; calling $order->get_billing_email() would fatal. Also, without verifying that the order (or sub‑order) and product belong to the current vendor, a seller could grant access for other vendors’ orders/products.

🐛 Minimal guard for missing/invalid order
-    $order         = dokan()->order->get( $order_id );
+    $order         = dokan()->order->get( $order_id );
+    if ( ! $order || ! $order->get_id() ) {
+        return new WP_Error(
+            'dokan_rest_invalid_order',
+            __( 'Invalid order ID.', 'dokan-lite' ),
+            [ 'status' => 404 ]
+        );
+    }

Please also add ownership checks (e.g., ensure the order/sub‑order and each product belong to the current vendor before granting).

🤖 Prompt for AI Agents
In `@includes/REST/ProductController.php` around lines 1811 - 1834, The
grant_downloadable_access handler must first ensure the order returned by
dokan()->order->get($order_id) is a valid object before calling
$order->get_billing_email(), and must verify ownership so a vendor cannot grant
access for other vendors' orders/products. Fix by: validate $order is not
null/false and return a proper REST error if missing; fetch the current vendor
ID (e.g. dokan_get_current_user_id()) and check that the order (or sub‑order)
belongs to that vendor and that each $product (from $product->get_id() /
$product post author) belongs to the same vendor before proceeding to build
$granted_files; if any ownership check fails, skip that product or return a
permission error. Implement these checks in grant_downloadable_access around the
existing dokan()->order->get(...) and product loop.

Comment on lines 1860 to 1866
if ( $download ) {
if ( $remaining ) {
$download->set_downloads_remaining( $remaining );
}
if ( $expiry ) {
$download->set_access_expires( $expiry );
}
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

Allow download_remaining = 0 (and explicit expiry updates).

Using truthy checks prevents setting zero downloads remaining or explicitly clearing/updating expiry. Use null checks instead.

🐛 Proposed fix
-    if ( $remaining ) {
+    if ( null !== $remaining ) {
         $download->set_downloads_remaining( $remaining );
     }
-    if ( $expiry ) {
+    if ( null !== $expiry && '' !== $expiry ) {
         $download->set_access_expires( $expiry );
     }
🤖 Prompt for AI Agents
In `@includes/REST/ProductController.php` around lines 1860 - 1866, The current
truthy checks prevent setting downloads remaining to 0 or explicitly clearing
expiry; modify the conditional tests around $remaining and $expiry in the
ProductController download update block so they check for null (e.g. $remaining
!== null and $expiry !== null) before calling
$download->set_downloads_remaining(...) and $download->set_access_expires(...),
ensuring zero values and explicit expiry updates are applied while leaving other
values untouched.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@includes/REST/OrderControllerV2.php`:
- Around line 244-245: The code is skipping updates to download_remaining and
access_expires when values are zero or when an existing permission branch
executes; change the logic to check for presence of params (use isset or a
param-exists helper rather than truthy checks) for $remaining and $expiry, apply
those values to the permission object in both the "create" and "existing
permission" branches, and ensure you call the same save/update method after
applying them so explicit 0/empty values clear or update the stored fields;
reference the variables $remaining, $expiry and the permission create/update
blocks to locate where to apply the changes.
🧹 Nitpick comments (1)
includes/REST/OrderControllerV2.php (1)

63-72: Add validation for download_remaining and access_expires parameters at the REST layer.

Both fields are persisted to the database without validation: download_remaining lacks bounds checking and could accept negative values, while access_expires accepts any string without verifying date format. Add sanitize/validate callbacks in the REST schema (consistent with other parameters in this controller) to ensure data integrity before WC_Customer_Download::save() is called.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@includes/REST/OrderControllerV2.php`:
- Around line 160-168: The bug is that the loop in OrderControllerV2 filtering
downloads checks an undefined $products_by_id, causing all items to be skipped;
update the condition to check the existing lookup map $products (i.e., replace
isset($products_by_id[$product_id]) with isset($products[$product_id])) and keep
the subsequent assignment $download->product = $products[$product_id] so
downloads are correctly collected and prepared for response in the same method.
🧹 Nitpick comments (1)
includes/REST/OrderControllerV2.php (1)

260-269: Redundant condition: $inserted_id is guaranteed truthy here.

The check at line 260 is unnecessary since lines 245-247 already continue when $inserted_id is falsy. Consider removing this nesting to reduce indentation.

♻️ Proposed simplification
                 $download->save();
             }
 
-            if ( $inserted_id ) {
-                ++$file_counter;
-                if ( $file->get_name() ) {
-                    $file_count = $file->get_name();
-                } else {
-                    /* translators: numeric number of files */
-                    $file_count = sprintf( __( 'File %d', 'dokan-lite' ), $file_counter );
-                }
-                $data[ $inserted_id ] = $file_count;
+            ++$file_counter;
+            if ( $file->get_name() ) {
+                $file_count = $file->get_name();
+            } else {
+                /* translators: numeric number of files */
+                $file_count = sprintf( __( 'File %d', 'dokan-lite' ), $file_counter );
             }
+            $data[ $inserted_id ] = $file_count;
         }
     }

Comment on lines +160 to +168
// Filter downloads with existing products and prepare response.
$downloads = [];
foreach ( $download_permissions as $download ) {
$product_id = intval( $download->product_id );
if ( isset( $products_by_id[ $product_id ] ) ) {
$download->product = $products[ $product_id ];
$downloads[] = $this->prepare_data_for_response( $download, $request );
}
}
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 | 🔴 Critical

Critical bug: Undefined variable $products_by_id causes empty results.

Line 164 references $products_by_id but the lookup map is stored in $products (line 151-157). This condition will always evaluate to false since $products_by_id is undefined, resulting in an empty $downloads array.

🐛 Proposed fix
         // Filter downloads with existing products and prepare response.
         $downloads = [];
         foreach ( $download_permissions as $download ) {
             $product_id = intval( $download->product_id );
-            if ( isset( $products_by_id[ $product_id ] ) ) {
+            if ( isset( $products[ $product_id ] ) ) {
                 $download->product = $products[ $product_id ];
                 $downloads[] = $this->prepare_data_for_response( $download, $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
// Filter downloads with existing products and prepare response.
$downloads = [];
foreach ( $download_permissions as $download ) {
$product_id = intval( $download->product_id );
if ( isset( $products_by_id[ $product_id ] ) ) {
$download->product = $products[ $product_id ];
$downloads[] = $this->prepare_data_for_response( $download, $request );
}
}
// Filter downloads with existing products and prepare response.
$downloads = [];
foreach ( $download_permissions as $download ) {
$product_id = intval( $download->product_id );
if ( isset( $products[ $product_id ] ) ) {
$download->product = $products[ $product_id ];
$downloads[] = $this->prepare_data_for_response( $download, $request );
}
}
🧰 Tools
🪛 PHPMD (2.15.0)

164-164: Avoid unused local variables such as '$products_by_id'. (undefined)

(UnusedLocalVariable)

🤖 Prompt for AI Agents
In `@includes/REST/OrderControllerV2.php` around lines 160 - 168, The bug is that
the loop in OrderControllerV2 filtering downloads checks an undefined
$products_by_id, causing all items to be skipped; update the condition to check
the existing lookup map $products (i.e., replace
isset($products_by_id[$product_id]) with isset($products[$product_id])) and keep
the subsequent assignment $download->product = $products[$product_id] so
downloads are correctly collected and prepared for response in the same method.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants