wc-downloadable-products
Implement, extend, or audit WooCommerce downloadable products and customer download access. Covers WC_Product_Download, stable download IDs, product CRUD, approved directories, the customer-download permission table and WC_Customer_Download CRUD, order-based grants and safe regen
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/woocommerce/wc-downloadable-products
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
WooCommerce downloadable products
Use WooCommerce's product and customer-download objects. A downloadable product definition is not itself customer access, and a permission row is not sufficient unless the related order currently permits downloads.
Start with the three-layer model
| Layer | Canonical representation | Answers |
|---|---|---|
| Product file | WC_Product_Download in $product->get_downloads() |
What file can this product deliver? |
| Entitlement | WC_Customer_Download |
Who may download which stable file ID, through which order, until when/how often? |
| Delivery | WC_Download_Handler |
Does this request pass every check, how is the file served, and is the attempt counted? |
Never infer access from _downloadable_files, product ownership, or a permission row alone. Resolve the actual order and use its is_download_permitted() gate.
Choose the task path
- Creating or changing product files: use product CRUD and preserve existing download IDs.
- Granting after purchase: let Woo's processing/completed lifecycle call
wc_downloadable_product_permissions(). - Adding one deliberate entitlement: call
wc_downloadable_file_permission()with a real product and order. - Regenerating an order: delete that order's existing permissions with the customer-download data store, then force the canonical grant.
- Displaying customer downloads: use
wc_get_customer_available_downloads()or order downloadable-item APIs. - Debugging security, storage, limits, REST, tracking, or duplicate rows: load references/core-download-contract.md.
Create or update a downloadable product
$product = new WC_Product_Simple();
$product->set_name( 'Field guide' );
$product->set_regular_price( '29.00' );
$product->set_virtual( true );
$product->set_downloadable( true );
$product->set_download_limit( 3 ); // -1 means unlimited.
$product->set_download_expiry( 30 ); // Days; -1 means no expiry.
$file = new WC_Product_Download();
$file->set_id( wp_generate_uuid4() );
$file->set_name( 'PDF guide' );
$file->set_file( $protected_download_url );
$product->set_downloads( array( $file ) );
$product->save();
set_downloads() validates new or changed paths. Local files must exist, use an allowed type, and pass Approved Download Directory checks when that feature is enabled. Remote absolute URLs are accepted without a remote existence request; shortcode providers must enforce their own validation.
WooCommerce 11.0 correctly resolves root-relative content paths against relocated/custom WP_CONTENT_DIR locations and requires a path-segment boundary. Do not assume content is literally /wp-content, slice a hard-coded 11 characters, or treat /application as if it were under /app. Store paths through product CRUD and let WC_Product_Download resolve/validate them.
For an existing file, reuse its ID:
$downloads = $product->get_downloads();
$file = $downloads[ $download_id ];
$file->set_name( 'Updated label' );
$file->set_file( $new_path );
$product->set_downloads( $downloads );
$product->save();
Since WooCommerce 3.3, generated download IDs are UUIDs, not file hashes. Replacing an ID strands old permission rows; preserving it intentionally moves existing entitlements to the new file. Treat that as an access migration decision, not a cosmetic edit.
Grant access through the order lifecycle
Woo hooks wc_downloadable_product_permissions() to processing and completed statuses. It skips an already-granted order unless forced, respects the "grant after payment" setting for processing orders, and creates one permission per line-item file.
// One explicit file permission. Product limits are multiplied by quantity.
$permission_id = wc_downloadable_file_permission(
$download_id,
$product,
$order,
$item->get_quantity(),
$item
);
Do not call this repeatedly as an idempotent operation: the table has no unique constraint. Likewise, $force = true does not remove old rows.
Safe full regeneration:
$store = WC_Data_Store::load( 'customer-download' );
$store->delete_by_order_id( $order->get_id() ); // Also removes related download logs.
$order->get_data_store()->set_download_permissions_granted( $order, false );
wc_downloadable_product_permissions( $order->get_id(), true );
Regeneration resets counters and expiry. Require an authorized admin action, log it, and never perform it on ordinary page loads.
Read access at the correct level
$available = wc_get_customer_available_downloads( $customer_id );
This is preferable to querying the table: it excludes exhausted/expired permissions, verifies the order's current download state, confirms that the product and stable file ID still exist, and rejects disabled files.
For diagnostics, wc_get_customer_download_permissions() returns permission objects filtered by the data store, but it is not the final display/access decision. Do not reconstruct download URLs from product meta.
Secure the delivery boundary
Woo download links contain the order key, customer email (or hash), product ID, and download ID. They are bearer-like secrets, not WordPress nonces. Do not put them in public caches, analytics URLs, support screenshots, or logs.
- Prefer the Woo protected uploads directory or private object storage with controlled delivery.
- A Media Library/public object URL can bypass Woo's handler, limits, expiry, login requirement, and logs.
- Approved directories are a path allowlist, not web-server access control.
- Prefer
forcefor local protected files, or correctly configuredxsendfile/X-Accel-Redirectfor scale. redirectexposes the source URL; use it only when the target is independently protected or intentionally public.- The login-required setting only adds an ownership check when the permission has a nonzero user ID. Guest-order links remain bearer links.
- Never replace
woocommerce_download_product_filepathfrom untrusted request input.
Preserve lifecycle invariants
- Product variation permissions use the variation ID.
downloads_remaining = ''means unlimited; do not coerce it to zero.- Expiry is calculated from completion date when available, otherwise from grant time.
- Order deletion through Woo CRUD cleans permissions and logs; direct SQL can orphan data.
- Fully refunded line items are excluded from order downloadable items.
- Creating a customer account can reassign and regenerate guest-order permissions.
- Range requests may be counted later through Action Scheduler to avoid charging one segmented download multiple times.
Audit checklist
- Confirm file URLs cannot bypass Woo authorization.
- Confirm download IDs remain stable unless revocation/migration is intended.
- Confirm grants are idempotent and not duplicated by retries or forced calls.
- Confirm the related order still controls access.
- Test exhausted, expired, guest, logged-in wrong-user, refunded, removed-file, and disabled-file cases.
- Test actual server delivery for
force,xsendfile, or redirect; PHP-only tests cannot validate web-server protection. - Treat download URLs, order keys, customer email, and logs as sensitive data.
Cross-references
- Use
wc-order-lifecycle-and-itemsfor order status, payment, refund, and item behavior. - Use
wc-action-scheduler-jobsfor partial-download tracking or background reconciliation.
References
- Verified source paths:
wp-content/plugins/woocommerce/includes/abstracts/abstract-wc-product.phpwp-content/plugins/woocommerce/includes/class-wc-product-download.phpwp-content/plugins/woocommerce/includes/class-wc-customer-download.phpwp-content/plugins/woocommerce/includes/class-wc-download-handler.phpwp-content/plugins/woocommerce/includes/wc-order-functions.phpwp-content/plugins/woocommerce/includes/wc-user-functions.phpwp-content/plugins/woocommerce/includes/data-stores/class-wc-customer-download-data-store.phpwp-content/plugins/woocommerce/src/Internal/ProductDownloads/wp-content/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Orders/ActionController.php
Files (wp-agent-skills)
-
agents
-
openai.yaml 239 B
interface: display_name: "WooCommerce Downloadable Products" short_description: "Secure WooCommerce download permissions" default_prompt: "Use $wc-downloadable-products to implement or audit WooCommerce downloadable product access."
-
-
references
-
core-download-contract.md 11 KB
# WooCommerce core download contract Version scope: WooCommerce 11.0.0, PHP 7.4+. Use this reference when auditing persistence, request authorization, delivery, REST exposure, or lifecycle maintenance. ## Product definition and storage `WC_Product_Download` carries `id`, `name`, `file`, `enabled`, plus forward-compatible extra data. `WC_Product::set_downloads()` converts arrays to objects, generates a UUID when an array has no `download_id`, validates the files, and indexes the result by download ID. Product data is persisted through Woo CRUD. The current CPT store uses: | Product meta | Meaning | |---|---| | `_downloadable` | Whether the product is downloadable. | | `_downloadable_files` | Map keyed by stable download ID; each entry includes name, file, enabled, and extra data. | | `_download_limit` | Per-file count; `-1` becomes unlimited access. | | `_download_expiry` | Days after grant/completion; `-1` means no expiry. | Do not write this meta directly. Validation and future storage compatibility live in product CRUD. For new/changed files, `check_is_valid()` enforces: - enabled state; - allowed MIME/extension for local server files; - local existence; - approved-directory membership when mode is enabled. Absolute remote HTTP(S) URLs are considered remote and are not fetched to confirm existence. Shortcodes are allowed; approved-directory validation resolves them by default, but the shortcode provider remains responsible for safe output. An invalid existing path is retained as disabled during hydration; a new or changed invalid path raises a product error. Never silently re-enable it. For a stored path of `relative` type, WooCommerce 11.0 resolves ordinary/`..` relative forms against `ABSPATH` and root-relative paths under the actual `WP_CONTENT_DIR`. The content-directory comparison is segment-aware, so a custom `/app` directory does not falsely match `/application`. Extensions must not hard-code `/wp-content`, assume `WP_CONTENT_DIR` sits below `ABSPATH`, or duplicate core's filesystem resolution. ## Approved directories and storage protection Approved locations live in `{$wpdb->prefix}wc_product_download_directories`. Mode is stored in `wc_downloads_approved_directories_mode`; production installations normally use `enabled`. The synchronizer discovers product download directories in Action Scheduler batches under: ```text hook: woocommerce_download_dir_sync group: woocommerce-db-updates ``` Discovered paths may require administrator review before they are enabled. Use Woo's settings/UI and public product CRUD; `Automattic\WooCommerce\Internal\ProductDownloads\ApprovedDirectories\Register` is internal and is not a stable integration API. Woo's protected upload directory is normally under `uploads/woocommerce_uploads`. Apache protection depends on the generated `.htaccess`; Nginx requires equivalent server configuration. Filename randomization and the approved-directory list do not prevent direct HTTP access. ## Entitlement tables `{$wpdb->prefix}woocommerce_downloadable_product_permissions`: | Column | Contract | |---|---| | `permission_id` | Primary key. | | `download_id` | Stable product file ID, `varchar(36)`. | | `product_id` | Product or variation ID. | | `order_id`, `order_key` | The order that owns the grant. | | `user_email`, `user_id` | Bearer-link identity plus optional account owner. | | `downloads_remaining` | Numeric text or empty string for unlimited. | | `access_granted`, `access_expires` | Grant and nullable expiry dates. | | `download_count` | Successful/recorded attempt count. | There is no unique constraint over order/product/download/customer. Application code must make retries idempotent. `{$wpdb->prefix}wc_download_log` records timestamp, permission ID, nullable user ID, and IP address. It has an index but no foreign key to permissions. Delete through the customer-download data store so logs are cleaned too. ## Canonical grant calculation `wc_downloadable_product_permissions( $order_id, $force )`: 1. Loads the order and checks its `download_permissions_granted` property unless forced. 2. For a processing order, stops when `woocommerce_downloads_grant_access_after_payment` is `no`. 3. Iterates downloadable order line items and current product files. 4. Calls `wc_downloadable_file_permission()` per file. 5. Sets the granted flag and fires `woocommerce_grant_product_download_permissions`. `wc_downloadable_file_permission()` creates `WC_Customer_Download` data: - actual product/variation ID; - customer ID, billing email, order ID, and order key; - product limit multiplied by line quantity, or empty string for unlimited; - grant time and count zero; - expiry days based on the order completion date when present, otherwise current time. Important filters/actions: | Hook | Purpose | |---|---| | `woocommerce_downloadable_file_permission` | Change the `WC_Customer_Download` object before save. | | `woocommerce_downloadable_file_permission_data` | Change insert data; security/ownership integrations such as gifting use this layer. | | `woocommerce_downloadable_file_permission_format` | Change SQL formats when insert data changes. | | `woocommerce_grant_product_download_access` | Observe one saved permission. | | `woocommerce_grant_product_download_permissions` | Observe the completed order-level grant. | Do not use `$force = true` as deduplication. Core's admin and REST v4 reset flow deletes by order ID first, then force-grants. ## Availability and request authorization `wc_get_customer_download_permissions()` asks the data store for non-expired/non-exhausted permission records. `wc_get_customer_available_downloads()` adds the final presentation checks: 1. related order exists; 2. `$order->is_download_permitted()` is true; 3. current product exists; 4. stable download ID still exists on that product; 5. file entry is enabled. The front-end URL normally contains: ```text download_file=<product-or-variation-id> order=<order-key> email=<customer-email> OR uid=<email-hash> key=<download-id> ``` `WC_Download_Handler` then verifies, in substance: 1. product, download ID, and enabled file; 2. order key and email or constant-time email hash; 3. matching permission row; 4. related order's `is_download_permitted()` result; 5. remaining count and expiry; 6. login and `download_file` ownership capability when login is required and the permission has a user ID. It fires `woocommerce_download_product`, tracks the attempt, and delegates to the configured delivery method. This URL is not a nonce and has no nonce lifetime. ## Delivery methods and request counting | `woocommerce_file_download_method` | Behavior | Main risk | |---|---|---| | `force` | PHP streams the file. Default. | PHP worker/memory/I/O pressure for large files. | | `xsendfile` | Delegates after authorization. | Requires correct web-server module/header mapping. | | `redirect` | Sends the browser to the file URL. | Source URL becomes visible and may bypass all future checks. | Remote force downloads can fall back to redirect only when `woocommerce_downloads_redirect_fallback_allowed` permits it. The inline setting changes `Content-Disposition` where Woo controls headers; it cannot control an external redirect target. Tracking atomically increments `download_count`, decrements a finite remaining count, clamps at zero, and writes a log. Range requests can be tracked later through the unique `track_partial_download` Action Scheduler job after the configured window (30 minutes by default), preventing one segmented/iOS transfer from consuming many attempts. Therefore immediate counts can be temporarily behind reality. ## Settings that alter semantics | Option | Typical default | Effect | |---|---|---| | `woocommerce_file_download_method` | `force` | Delivery implementation. | | `woocommerce_downloads_require_login` | `no` | Adds account ownership enforcement only for permissions with a user ID. | | `woocommerce_downloads_grant_access_after_payment` | `yes` | Allows processing orders to receive/access downloads. | | `wc_downloads_approved_directories_mode` | install-dependent initialization, normally `enabled` | Enforces download-source allowlist. | | `woocommerce_downloads_redirect_fallback_allowed` | `no` | Allows force mode to redirect when a remote file cannot be streamed. | | `woocommerce_downloads_deliver_inline` | disabled | Uses inline browser display where Woo controls the response; redirects ignore it. | | `woocommerce_downloads_add_hash_to_filename` | `yes` | Adds a unique suffix to newly uploaded filenames; not an authorization boundary. | | `woocommerce_downloads_count_partial` | `yes` | Enables deferred counting behavior for partial/range downloads. | Read defaults from the running Woo version; do not treat a missing option row as a literal `no` without checking the caller's fallback. ## REST and headless boundary WC REST v3 exposes authenticated customer downloads at `GET /wc/v3/customers/{customer_id}/downloads`; it is a read-only projection and requires Woo customer read permission. The response can contain live download URLs and file data. Enforce ownership/capability on any custom proxy, avoid shared caches, redact telemetry, and use HTTPS. WC REST v4 provides an authorized order action named `reset_download_permissions`. It follows delete-then-force-grant behavior. Plugin-defined public routes should not expose this operation without a strict capability and order-scope check. Headless clients should receive the Woo-authorized URL only after authenticating to the application's own API. Do not expose raw `_downloadable_files` or storage URLs as a substitute for customer-download projection. ## Change and cleanup semantics - Removing a download ID makes existing rows unusable but does not itself guarantee their physical deletion. - Preserving an ID while changing its file makes existing permissions deliver the replacement. - Core's `DownloadPermissionsAdjuster` handles a narrow simple-to-variable conversion case by copying equivalent permissions to matching child variations; it is not a general permission migration service. - Order CRUD deletion calls the customer-download cleanup path. Direct order-table/post deletion does not provide that contract. - New-account guest-order association can delete and regenerate download permissions with a user ID. - Fully refunded line items are omitted from `WC_Order::get_downloadable_items()`. For bulk migration, define an explicit policy for stable IDs, existing counts, expiry, removed files, rollback, and idempotency before touching customer data. ## Minimum regression matrix Test all applicable cases: - processing with grant-after-payment both enabled and disabled; - completed order; - finite limit, unlimited empty-string limit, exhausted permission; - future and expired dates; - logged-out guest, logged-in owner, logged-in different user; - variation file and parent product distinction; - removed ID, preserved ID with changed file, disabled invalid file; - refunded line item; - duplicate retry and explicit reset; - range request/deferred tracking; - direct storage URL versus authorized Woo URL; - real Apache/Nginx/object-storage delivery for the configured method.
-
-
SKILL.md 8.9 KB
--- name: wc-downloadable-products description: Implement, extend, or audit WooCommerce downloadable products and customer download access. Covers WC_Product_Download, stable download IDs, product CRUD, approved directories, the customer-download permission table and WC_Customer_Download CRUD, order-based grants and safe regeneration, limits and expiry, My Account and REST reads, bearer download URLs, download methods, logging, partial requests, and protected file storage. Use for _downloadable_files, wc_downloadable_product_permissions(), wc_downloadable_file_permission(), WC_Download_Handler, missing/duplicate/expired downloads, private digital files, or code that grants and revokes WooCommerce downloads. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "woocommerce" wp-skills-plugin-version-tested: "11.0.0" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-05" --- # WooCommerce downloadable products Use WooCommerce's product and customer-download objects. A downloadable product definition is not itself customer access, and a permission row is not sufficient unless the related order currently permits downloads. ## Start with the three-layer model | Layer | Canonical representation | Answers | |---|---|---| | Product file | `WC_Product_Download` in `$product->get_downloads()` | What file can this product deliver? | | Entitlement | `WC_Customer_Download` | Who may download which stable file ID, through which order, until when/how often? | | Delivery | `WC_Download_Handler` | Does this request pass every check, how is the file served, and is the attempt counted? | Never infer access from `_downloadable_files`, product ownership, or a permission row alone. Resolve the actual order and use its `is_download_permitted()` gate. ## Choose the task path - Creating or changing product files: use product CRUD and preserve existing download IDs. - Granting after purchase: let Woo's processing/completed lifecycle call `wc_downloadable_product_permissions()`. - Adding one deliberate entitlement: call `wc_downloadable_file_permission()` with a real product and order. - Regenerating an order: delete that order's existing permissions with the customer-download data store, then force the canonical grant. - Displaying customer downloads: use `wc_get_customer_available_downloads()` or order downloadable-item APIs. - Debugging security, storage, limits, REST, tracking, or duplicate rows: load [references/core-download-contract.md](references/core-download-contract.md). ## Create or update a downloadable product ```php $product = new WC_Product_Simple(); $product->set_name( 'Field guide' ); $product->set_regular_price( '29.00' ); $product->set_virtual( true ); $product->set_downloadable( true ); $product->set_download_limit( 3 ); // -1 means unlimited. $product->set_download_expiry( 30 ); // Days; -1 means no expiry. $file = new WC_Product_Download(); $file->set_id( wp_generate_uuid4() ); $file->set_name( 'PDF guide' ); $file->set_file( $protected_download_url ); $product->set_downloads( array( $file ) ); $product->save(); ``` `set_downloads()` validates new or changed paths. Local files must exist, use an allowed type, and pass Approved Download Directory checks when that feature is enabled. Remote absolute URLs are accepted without a remote existence request; shortcode providers must enforce their own validation. WooCommerce 11.0 correctly resolves root-relative content paths against relocated/custom `WP_CONTENT_DIR` locations and requires a path-segment boundary. Do not assume content is literally `/wp-content`, slice a hard-coded 11 characters, or treat `/application` as if it were under `/app`. Store paths through product CRUD and let `WC_Product_Download` resolve/validate them. For an existing file, reuse its ID: ```php $downloads = $product->get_downloads(); $file = $downloads[ $download_id ]; $file->set_name( 'Updated label' ); $file->set_file( $new_path ); $product->set_downloads( $downloads ); $product->save(); ``` Since WooCommerce 3.3, generated download IDs are UUIDs, not file hashes. Replacing an ID strands old permission rows; preserving it intentionally moves existing entitlements to the new file. Treat that as an access migration decision, not a cosmetic edit. ## Grant access through the order lifecycle Woo hooks `wc_downloadable_product_permissions()` to processing and completed statuses. It skips an already-granted order unless forced, respects the "grant after payment" setting for processing orders, and creates one permission per line-item file. ```php // One explicit file permission. Product limits are multiplied by quantity. $permission_id = wc_downloadable_file_permission( $download_id, $product, $order, $item->get_quantity(), $item ); ``` Do not call this repeatedly as an idempotent operation: the table has no unique constraint. Likewise, `$force = true` does not remove old rows. Safe full regeneration: ```php $store = WC_Data_Store::load( 'customer-download' ); $store->delete_by_order_id( $order->get_id() ); // Also removes related download logs. $order->get_data_store()->set_download_permissions_granted( $order, false ); wc_downloadable_product_permissions( $order->get_id(), true ); ``` Regeneration resets counters and expiry. Require an authorized admin action, log it, and never perform it on ordinary page loads. ## Read access at the correct level ```php $available = wc_get_customer_available_downloads( $customer_id ); ``` This is preferable to querying the table: it excludes exhausted/expired permissions, verifies the order's current download state, confirms that the product and stable file ID still exist, and rejects disabled files. For diagnostics, `wc_get_customer_download_permissions()` returns permission objects filtered by the data store, but it is not the final display/access decision. Do not reconstruct download URLs from product meta. ## Secure the delivery boundary Woo download links contain the order key, customer email (or hash), product ID, and download ID. They are bearer-like secrets, not WordPress nonces. Do not put them in public caches, analytics URLs, support screenshots, or logs. - Prefer the Woo protected uploads directory or private object storage with controlled delivery. - A Media Library/public object URL can bypass Woo's handler, limits, expiry, login requirement, and logs. - Approved directories are a path allowlist, not web-server access control. - Prefer `force` for local protected files, or correctly configured `xsendfile`/`X-Accel-Redirect` for scale. - `redirect` exposes the source URL; use it only when the target is independently protected or intentionally public. - The login-required setting only adds an ownership check when the permission has a nonzero user ID. Guest-order links remain bearer links. - Never replace `woocommerce_download_product_filepath` from untrusted request input. ## Preserve lifecycle invariants - Product variation permissions use the variation ID. - `downloads_remaining = ''` means unlimited; do not coerce it to zero. - Expiry is calculated from completion date when available, otherwise from grant time. - Order deletion through Woo CRUD cleans permissions and logs; direct SQL can orphan data. - Fully refunded line items are excluded from order downloadable items. - Creating a customer account can reassign and regenerate guest-order permissions. - Range requests may be counted later through Action Scheduler to avoid charging one segmented download multiple times. ## Audit checklist 1. Confirm file URLs cannot bypass Woo authorization. 2. Confirm download IDs remain stable unless revocation/migration is intended. 3. Confirm grants are idempotent and not duplicated by retries or forced calls. 4. Confirm the related order still controls access. 5. Test exhausted, expired, guest, logged-in wrong-user, refunded, removed-file, and disabled-file cases. 6. Test actual server delivery for `force`, `xsendfile`, or redirect; PHP-only tests cannot validate web-server protection. 7. Treat download URLs, order keys, customer email, and logs as sensitive data. ## Cross-references - Use `wc-order-lifecycle-and-items` for order status, payment, refund, and item behavior. - Use `wc-action-scheduler-jobs` for partial-download tracking or background reconciliation. ## References - Verified source paths: - `wp-content/plugins/woocommerce/includes/abstracts/abstract-wc-product.php` - `wp-content/plugins/woocommerce/includes/class-wc-product-download.php` - `wp-content/plugins/woocommerce/includes/class-wc-customer-download.php` - `wp-content/plugins/woocommerce/includes/class-wc-download-handler.php` - `wp-content/plugins/woocommerce/includes/wc-order-functions.php` - `wp-content/plugins/woocommerce/includes/wc-user-functions.php` - `wp-content/plugins/woocommerce/includes/data-stores/class-wc-customer-download-data-store.php` - `wp-content/plugins/woocommerce/src/Internal/ProductDownloads/` - `wp-content/plugins/woocommerce/src/Internal/RestApi/Routes/V4/Orders/ActionController.php`
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.