feat(export): post-Memberships subscription & user CSV exports (NPPD-1730)#499
feat(export): post-Memberships subscription & user CSV exports (NPPD-1730)#499adekbadek wants to merge 11 commits into
Conversation
…NPPD-1730) Red phase: mocks for WC_CSV_Batch_Exporter and WCS query functions, plus unit tests covering row building, list-param translation (CPT + HPOS), the extensibility filter contract, CSV-injection escaping, and paging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
…-1730) Green phase: Subscriptions_CSV_Exporter and Users_CSV_Exporter extend WooCommerce's WC_CSV_Batch_Exporter, translating captured admin-list filters into paged queries (offset-based; wcs_get_subscriptions' paged arg is broken) with headers/row/query-args filters for NPPD-1719. Promotes Group_Subscription_Settings::apply_group_filter to public for reuse by the exporter (backward-compatible visibility widening). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
…ownload (NPPD-1730) Export CSV buttons on the subscriptions (HPOS + CPT) and users list tables export the current filtered view: browser-driven page stepping via wp_ajax_newspack_csv_export, temp files in a hardened uploads/newspack-csv-exports/ dir with server-generated random filenames, nonce + capability-checked download that deletes on send, and a daily cleanup cron for abandoned files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
…PPD-1730) Support-driven equivalents of the admin export buttons, sharing the exporters and query translation. Uses a fresh exporter instance per page (the WC batch exporter's row counter accumulates per instance, which would otherwise end multi-page exports early). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
…1730) Without this, the object cache accumulates every loaded subscription in the long-running CLI process and a 40k-row export exhausts the 512M memory limit around 26k rows. The admin AJAX flow is unaffected (one page per request). Verified: 40k rows in 1m24s with flat memory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
There was a problem hiding this comment.
Pull request overview
Adds first-party CSV exports to newspack-plugin to replace the WooCommerce Memberships export after Access Control migrations, supporting both admin list-table exports (AJAX-stepped) and WP-CLI exports, with extensibility filters for adding columns.
Changes:
- Introduces batched CSV exporters for subscriptions and users (WooCommerce
WC_CSV_Batch_Exporter-style), including query-param → query-args translation and filter hooks for headers/rows/query args. - Adds an admin UI “Export CSV” button to Subscriptions (HPOS + legacy CPT) and Users list tables, driven by a new
csv-exportwebpack entry and an AJAX-stepped export/download flow. - Adds WP-CLI commands (
wp newspack export-subscriptions,wp newspack export-users) sharing the same exporter logic, plus unit tests and supporting Woo/WCS mocks.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| plugins/newspack-plugin/webpack.config.js | Registers a new standalone webpack entry for the admin CSV export driver. |
| plugins/newspack-plugin/src/csv-export/index.js | Implements the browser-driven stepped export flow via AJAX + download redirect. |
| plugins/newspack-plugin/includes/export/class-csv-exports.php | Adds list-table buttons, AJAX handler, download handler, and stale-file cleanup cron. |
| plugins/newspack-plugin/includes/export/class-csv-batch-exporter.php | Adds a base exporter that stages temp files in a hardened uploads subdirectory and supports CLI save-to. |
| plugins/newspack-plugin/includes/export/class-subscriptions-csv-exporter.php | Implements subscriptions CSV rows and list-filter → query translation. |
| plugins/newspack-plugin/includes/export/class-users-csv-exporter.php | Implements users CSV rows (including WC address meta) and users-list filter replay. |
| plugins/newspack-plugin/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php | Makes apply_group_filter() public for reuse by the subscriptions exporter. |
| plugins/newspack-plugin/includes/cli/class-initializer.php | Registers new newspack export-subscriptions / newspack export-users WP-CLI commands. |
| plugins/newspack-plugin/includes/cli/class-export.php | Implements the CLI export commands using the shared exporters and paged batching. |
| plugins/newspack-plugin/includes/class-newspack.php | Bootstraps the CSV exports controller. |
| plugins/newspack-plugin/tests/mocks/wc-mocks.php | Extends Woo/WCS mocks to support CSV exporter behavior and subscription querying. |
| plugins/newspack-plugin/tests/unit-tests/export/class-subscriptions-csv-exporter.php | Adds unit tests for subscription export row building, filters, paging, and CSV injection escaping. |
| plugins/newspack-plugin/tests/unit-tests/export/class-users-csv-exporter.php | Adds unit tests for user export row building, query args translation, paging, and extensibility filters. |
Shared translated address column labels (replacing the users exporter's untranslatable ucwords labels), a bail-out when the exports dir cannot be created, and the public-path import in the csv-export entry. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
- Bind download filenames to their capability-checked export type and reject cross-type replays; shared filename generation for admin + CLI. - Stream save_to() (flat memory at any size) and keep temp files when the destination write fails; no error silencing. - Stable ID ASC export ordering (offset paging stays consistent when rows are created mid-export; both order datastores map 'ID'). - Cleanup cron: armed lazily by the first export, cleared on plugin deactivation, honors NEWSPACK_CRON_DISABLE. - Guard array-shaped request params (graceful degrade, no TypeErrors); skip the users_list_table_query_args replay outside admin (its callbacks may assume admin-only APIs, fatal under WP-CLI). - Users export requires WooCommerce active (manage_woocommerce persists after WC deactivation); prime user meta caches per page; prefetch subscription customers with cache_users(). - A11y: button caveat via aria-describedby; live region announces only start/completion/errors instead of every percentage step. - Revert the public-path import in the csv-export entry: it splits the webpack runtime into the commons chunk, which standalone entries do not enqueue, leaving the entry module never executed (verified broken in the env, then verified working after revert). - Native types on the new method surface; tests for the filename contract, save_to failure handling, per-instance counter accumulation (why every flow uses a fresh exporter per page), array-param guards, and the stale-file sweep. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
- save_to() fails instead of delivering a headers-only CSV when the data temp file is missing/unopenable (the CLI would otherwise report a phantom successful export over an unwritable uploads dir). - The users_list_table_query_args replay hands callbacks slashed, superglobal-shaped values and survives a callback that assumes list-table screen context admin-ajax can't provide (degrades to filter-not-honored instead of fataling the export step). - Download handler: consistent 403s for malformed links and HTTP 410 for replayed links whose file was already served and deleted. - Enqueue the export script only for users who can actually export on that screen; keep the button disabled while the download is served. - Default CLI output name reuses the shared filename generator; the remaining new signatures get native types; the stale-file sweep test covers the .csv.headers companions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
…PPD-1730) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ
chickenn00dle
left a comment
There was a problem hiding this comment.
Reviewed in an isolated env with WooCommerce + Subscriptions. No blockers — this is a careful, defense-in-depth implementation, and I confirmed the security-critical behaviors at runtime: CSV row counts match the DB across all-statuses/active/role/product filters, formula-injection is neutralized (=, +, -, @ prefixed with '; commas/quotes/newlines quoted), the empty dataset yields a clean headers-only file, and the capability gate holds (only an admin passes; editor/author/subscriber/customer are denied both exports). Cross-type filename guard and path-traversal rejection also hold; 29 unit tests pass and CI is green.
The comments below are all non-blocking. The one I'd most want a look at before merge is the classmap/autoload footgun — it's latent today but cheap to defuse. The rest are scale notes for very large sites plus a per-row deprecation notice I hit at runtime.
|
@chickenn00dle Re-requesting review – the review comments have been addressed. |
All Submissions:
Changes proposed in this Pull Request:
Deactivating WooCommerce Memberships (a standard Access Control migration step) removes the membership CSV export some publishers rely on for recurring operations – mailing lists, revenue-recognition user pulls, product segmentation. This adds a first-party replacement to newspack-plugin:
includes/export/):Subscriptions_CSV_ExporterandUsers_CSV_Exporterextend WooCommerce'sWC_CSV_Batch_Exporter(temp file in uploads, page stepping, CSV-injection escaping inherited). Subscriptions rows include schedule dates, products (multi-item concatenated), totals, customer identity, and full billing/shipping – the address data the old Memberships export never had. Users rows include core fields, roles, and WC billing/shipping meta.limit/offset-based viawcs_get_orders_with_meta_query()with a SQL count – deliberately notwcs_get_subscriptions( [ 'paged' => n ] ), whosepagedarg is silently dropped, and not the unpagedproduct_idpath. List sorting is intentionally not carried over (export order is always ID ascending — insert-stable, so offset pagination stays consistent when rows are created mid-export).uploads/newspack-csv-exports/dir with server-generated random filenames; a daily cron sweeps abandoned files.wp newspack export-subscriptions/wp newspack export-usersshare the same exporters and query translation, with per-batch object-cache clearing so large exports keep flat memory.newspack_{subscriptions,users}_export_{headers,row,query_args}filters let other features add columns – NPPD-1719 (group-subscription export) will use this exactly the way Memberships-for-Teams addedteam_*columns to the old Memberships export.Group_Subscription_Settings::apply_group_filter()from private to public for reuse by the subscriptions exporter (backward-compatible visibility widening).Capabilities: subscriptions export requires
manage_woocommerce; users export requireslist_usersandmanage_woocommerce(the CSV carries Woo billing PII thatlist_usersalone does not imply).Closes NPPD-1730.
How to test the changes in this Pull Request:
wp option update woocommerce_custom_orders_table_enabled no+ sync) and repeat on the legacy list – same result.uploads/newspack-csv-exports/holds no files after a completed download.=HYPERLINK("https://evil.example","x")and export – the cell arrives prefixed with'(formula injection neutralized).wp newspack export-subscriptions --status=active --product=<id> --output=/tmp/subs.csvandwp newspack export-users --role=subscriber --output=/tmp/users.csv– row counts match the equivalent admin exports.Other information:
🤖 Generated with Claude Code
https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ