Skip to content

feat(export): post-Memberships subscription & user CSV exports (NPPD-1730)#499

Open
adekbadek wants to merge 11 commits into
mainfrom
feat/csv-exports
Open

feat(export): post-Memberships subscription & user CSV exports (NPPD-1730)#499
adekbadek wants to merge 11 commits into
mainfrom
feat/csv-exports

Conversation

@adekbadek

@adekbadek adekbadek commented Jul 2, 2026

Copy link
Copy Markdown
Member

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:

  • Batched CSV exporters (includes/export/): Subscriptions_CSV_Exporter and Users_CSV_Exporter extend WooCommerce's WC_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.
  • Export the current filtered view: an "Export CSV" button on the subscriptions list (HPOS + legacy CPT) and the users list serializes the active list filters (status, product, customer, payment method, group, search, month / role, search) and translates them into paged queries. Paging is limit/offset-based via wcs_get_orders_with_meta_query() with a SQL count – deliberately not wcs_get_subscriptions( [ 'paged' => n ] ), whose paged arg is silently dropped, and not the unpaged product_id path. 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).
  • AJAX-stepped flow (WC product-exporter style): the browser drives one page per request, so no long request ever runs; the download is nonce-protected, capability re-checked, and deletes the file on send. Temp files live in a hardened uploads/newspack-csv-exports/ dir with server-generated random filenames; a daily cron sweeps abandoned files.
  • WP-CLI: wp newspack export-subscriptions / wp newspack export-users share the same exporters and query translation, with per-batch object-cache clearing so large exports keep flat memory.
  • Extensibility contract: 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 added team_* columns to the old Memberships export.
  • Promotes 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 requires list_users and manage_woocommerce (the CSV carries Woo billing PII that list_users alone does not imply).

Closes NPPD-1730.

How to test the changes in this Pull Request:

  1. On a site with WooCommerce + WC Subscriptions and some subscriptions, go to WooCommerce > Subscriptions, apply filters (e.g. status Active + a product), and click Export CSV. The progress indicator steps to 100% and a CSV downloads whose rows exactly match the filtered list (count and content).
  2. Toggle HPOS (wp option update woocommerce_custom_orders_table_enabled no + sync) and repeat on the legacy list – same result.
  3. Go to Users, filter by a role, click Export CSV – the CSV rows match the filtered user count, with billing/shipping meta columns.
  4. Try the download URL with a wrong nonce or as a user without the capability – it is rejected; confirm uploads/newspack-csv-exports/ holds no files after a completed download.
  5. Set a billing first name to =HYPERLINK("https://evil.example","x") and export – the cell arrives prefixed with ' (formula injection neutralized).
  6. CLI: wp newspack export-subscriptions --status=active --product=<id> --output=/tmp/subs.csv and wp newspack export-users --role=subscriber --output=/tmp/users.csv – row counts match the equivalent admin exports.
  7. Scale: seeded 40,000 subscriptions – browser export completes in ~2m45s (800 steps, constant pace), CLI in ~1m24s with flat memory.

Other information:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes, as applicable?
  • Have you successfully run tests with your changes locally?

🤖 Generated with Claude Code

https://claude.ai/code/session_01WquT3V87kgu5Bv2ihuXZUJ

adekbadek and others added 5 commits July 2, 2026 13:20
…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
Copilot AI review requested due to automatic review settings July 2, 2026 11:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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-export webpack 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.

Comment thread plugins/newspack-plugin/src/csv-export/index.js
Comment thread plugins/newspack-plugin/includes/export/class-users-csv-exporter.php Outdated
adekbadek and others added 4 commits July 2, 2026 14:02
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
@adekbadek
adekbadek marked this pull request as ready for review July 7, 2026 09:11
@adekbadek
adekbadek requested a review from a team as a code owner July 7, 2026 09:11

@chickenn00dle chickenn00dle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment thread plugins/newspack-plugin/includes/export/class-csv-exports.php Outdated
Comment thread plugins/newspack-plugin/includes/export/class-subscriptions-csv-exporter.php Outdated
Comment thread plugins/newspack-plugin/includes/export/class-subscriptions-csv-exporter.php Outdated
Comment thread plugins/newspack-plugin/includes/export/class-csv-exports.php Outdated
Comment thread plugins/newspack-plugin/includes/cli/class-export.php Outdated
@adekbadek
adekbadek requested a review from chickenn00dle July 18, 2026 21:43
@adekbadek

Copy link
Copy Markdown
Member Author

@chickenn00dle Re-requesting review – the review comments have been addressed.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants