Commit 95ee4754311 for woocommerce
commit 95ee4754311b809b17a3f6c26fe955702bd15056
Author: Cvetan Cvetanov <cvetan.cvetanov@automattic.com>
Date: Wed Jul 29 15:48:35 2026 +0300
Revert payment token query changes from #66607 (#67104)
* Revert payment token query changes
* Add changelog entry for payment token query revert
* Remove revert changelog entry
diff --git a/plugins/woocommerce/changelog/25025-payment-tokens-posts-per-page-limit b/plugins/woocommerce/changelog/25025-payment-tokens-posts-per-page-limit
deleted file mode 100644
index 20707f1d1ca..00000000000
--- a/plugins/woocommerce/changelog/25025-payment-tokens-posts-per-page-limit
+++ /dev/null
@@ -1,4 +0,0 @@
-Significance: patch
-Type: fix
-
-Saved payment methods are no longer capped by the posts_per_page option: customer token queries now default to a dedicated limit of 100 (filterable via woocommerce_get_customer_payment_tokens_limit), and the payment token data store no longer applies a LIMIT to queries scoped to a user_id or token_id unless one is explicitly requested, so the personal data eraser and user deletion clean up all tokens. Unscoped data store queries (no user_id, token_id or limit) match on unindexed columns and read the whole table, so they fall back to a ceiling of 500 rows, filterable via woocommerce_get_payment_tokens_unscoped_limit; pass an explicit limit to opt out.
diff --git a/plugins/woocommerce/includes/class-wc-payment-tokens.php b/plugins/woocommerce/includes/class-wc-payment-tokens.php
index b02fe795ad4..618b5dd7cc9 100644
--- a/plugins/woocommerce/includes/class-wc-payment-tokens.php
+++ b/plugins/woocommerce/includes/class-wc-payment-tokens.php
@@ -16,20 +16,10 @@ defined( 'ABSPATH' ) || exit;
*/
class WC_Payment_Tokens {
- /**
- * Default maximum number of tokens returned by `get_customer_tokens`.
- *
- * @since 11.1.0
- * @var int
- */
- const DEFAULT_CUSTOMER_TOKENS_LIMIT = 100;
-
/**
* Gets valid tokens from the database based on user defined criteria.
*
* @since 2.6.0
- * @since 11.1.0 Results are no longer implicitly limited by the `posts_per_page` option;
- * pass a positive `limit` arg to bound the query.
* @param array $args Query arguments {
* Array of query parameters.
*
@@ -37,11 +27,6 @@ class WC_Payment_Tokens {
* @type string $user_id User ID.
* @type string $gateway_id Gateway ID.
* @type string $type Token type.
- * @type int $limit Maximum number of tokens to return. When omitted or not positive, queries
- * scoped to a `token_id` or `user_id` are unlimited, while unscoped queries
- * fall back to a ceiling filterable via
- * `woocommerce_get_payment_tokens_unscoped_limit`.
- * @type int $page Page of results to return when `limit` is set. Default 1.
* }
* @return WC_Payment_Token[]
*/
@@ -93,11 +78,10 @@ class WC_Payment_Tokens {
* Controls the maximum number of Payment Methods that will be listed via the My Account page.
*
* @since 7.2.0
- * @since 11.1.0 The default changed from the value of the `posts_per_page` option to 100.
*
- * @param int $limit Maximum number of tokens to return. Defaults to 100.
+ * @param int $limit Defaults to the value of the `posts_per_page` option.
*/
- 'limit' => apply_filters( 'woocommerce_get_customer_payment_tokens_limit', self::DEFAULT_CUSTOMER_TOKENS_LIMIT ),
+ 'limit' => apply_filters( 'woocommerce_get_customer_payment_tokens_limit', get_option( 'posts_per_page' ) ),
)
);
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php b/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php
index 15a24749668..c6d6e5012b5 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-payment-token-data-store.php
@@ -23,19 +23,6 @@ class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Object_
*/
protected $meta_type = 'payment_token';
- /**
- * Fallback maximum number of rows returned by `get_tokens()` for queries that are neither
- * scoped to a `user_id`/`token_id` nor given an explicit `limit`.
- *
- * Such a query matches on `gateway_id`/`type` only, and neither column is indexed, so it is a
- * full table scan across every user's tokens. This ceiling keeps an accidental store-wide
- * query bounded; scoped queries ride the primary key or the `user_id` index and stay unlimited.
- *
- * @since 11.1.0
- * @var int
- */
- const DEFAULT_UNSCOPED_TOKENS_LIMIT = 500;
-
/**
* If we have already saved our extra data, don't do automatic / default handling.
*
@@ -241,16 +228,8 @@ class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Object_
* Accepts token_id, user_id, gateway_id, and type.
* Each object should contain the fields token_id, gateway_id, token, user_id, type, is_default.
*
- * Queries scoped to a `token_id` or `user_id` are unlimited unless a positive `limit` arg is
- * passed. An unscoped query (neither of those, nor a `limit`) reads every token in the store, so
- * it falls back to DEFAULT_UNSCOPED_TOKENS_LIMIT rows, filterable via
- * `woocommerce_get_payment_tokens_unscoped_limit`.
- *
* @since 3.0.0
- * @since 11.1.0 Results are no longer implicitly limited by the `posts_per_page` option; a `LIMIT`
- * clause is applied when a positive `limit` arg is passed, or as a fallback ceiling
- * on unscoped queries.
- * @param array $args List of accepted args: token_id, gateway_id, user_id, type, limit, page.
+ * @param array $args List of accepted args: token_id, gateway_id, user_id, type.
* @return array
*/
public function get_tokens( $args ) {
@@ -284,34 +263,11 @@ class WC_Payment_Token_Data_Store extends WC_Data_Store_WP implements WC_Object_
$gateway_ids = $gateways->get_payment_gateway_ids();
}
- $page = isset( $args['page'] ) ? max( 1, absint( $args['page'] ) ) : 1;
- $limit = isset( $args['limit'] ) ? absint( $args['limit'] ) : 0;
-
- // Without an explicit limit, a query scoped to a token_id or user_id stays unlimited:
- // consumers like the personal data eraser and user deletion cleanup rely on retrieving
- // every matching token, reaching this method via WC_Payment_Tokens::get_tokens(). An
- // unscoped query has no such natural bound, so it falls back to a ceiling instead.
- if ( $limit < 1 && ! $args['token_id'] && ! $args['user_id'] ) {
- /**
- * Controls the fallback maximum number of tokens returned by an unscoped query, i.e. one
- * passing neither `user_id`/`token_id` nor an explicit `limit`. Such a query matches on
- * the unindexed `gateway_id`/`type` columns and would otherwise read every token in the
- * store. Pass an explicit `limit` (and `page`) to opt out of this ceiling.
- *
- * Queries scoped to a `user_id` or `token_id` are unaffected and remain unlimited.
- *
- * @since 11.1.0
- *
- * @param int $limit Maximum number of tokens to return. Defaults to 500.
- * @param array $args The arguments passed to `get_tokens()`.
- */
- $limit = absint( apply_filters( 'woocommerce_get_payment_tokens_unscoped_limit', self::DEFAULT_UNSCOPED_TOKENS_LIMIT, $args ) );
- }
+ $page = isset( $args['page'] ) ? absint( $args['page'] ) : 1;
+ $posts_per_page = absint( isset( $args['limit'] ) ? $args['limit'] : get_option( 'posts_per_page' ) );
- $limits = '';
- if ( $limit > 0 ) {
- $limits = 'LIMIT ' . absint( ( $page - 1 ) * $limit ) . ', ' . $limit;
- }
+ $pgstrt = absint( ( $page - 1 ) * $posts_per_page ) . ', ';
+ $limits = 'LIMIT ' . $pgstrt . $posts_per_page;
$gateway_ids[] = '';
$where[] = "gateway_id IN ('" . implode( "','", array_map( 'esc_sql', $gateway_ids ) ) . "')";
diff --git a/plugins/woocommerce/includes/wc-user-functions.php b/plugins/woocommerce/includes/wc-user-functions.php
index ffdd45c9d17..708ac3b89ba 100644
--- a/plugins/woocommerce/includes/wc-user-functions.php
+++ b/plugins/woocommerce/includes/wc-user-functions.php
@@ -1061,11 +1061,8 @@ function wc_delete_user_data( $user_id ) {
)
);
- // Clean up payment tokens. Query without a limit so every token is removed, not just the
- // customer-facing subset capped by `get_customer_tokens()`. Deliberately bypasses the
- // `woocommerce_get_customer_payment_tokens` filter too: cleanup must not be narrowed by a
- // display-oriented filter, and this matches the personal data eraser, which also omits it.
- $payment_tokens = WC_Payment_Tokens::get_tokens( array( 'user_id' => $user_id ) );
+ // Clean up payment tokens.
+ $payment_tokens = WC_Payment_Tokens::get_customer_tokens( $user_id );
foreach ( $payment_tokens as $payment_token ) {
$payment_token->delete();
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-payment-tokens-test.php b/plugins/woocommerce/tests/php/includes/class-wc-payment-tokens-test.php
deleted file mode 100644
index dbb05e8f111..00000000000
--- a/plugins/woocommerce/tests/php/includes/class-wc-payment-tokens-test.php
+++ /dev/null
@@ -1,314 +0,0 @@
-<?php
-declare( strict_types = 1 );
-
-/**
- * @package WooCommerce\Tests\PaymentTokens
- */
-
-/**
- * Class WC_Payment_Tokens_Test.
- */
-class WC_Payment_Tokens_Test extends WC_Unit_Test_Case {
-
- /**
- * ID of the customer used in the tests.
- *
- * @var int
- */
- private $user_id;
-
- /**
- * Set up test fixtures.
- */
- public function setUp(): void {
- parent::setUp();
- $this->user_id = self::factory()->user->create( array( 'role' => 'customer' ) );
- }
-
- /**
- * Tear down test fixtures.
- */
- public function tearDown(): void {
- remove_all_filters( 'woocommerce_get_customer_payment_tokens_limit' );
- remove_all_filters( 'woocommerce_get_payment_tokens_unscoped_limit' );
- remove_all_filters( 'pre_option_posts_per_page' );
- update_option( 'posts_per_page', 10 );
- parent::tearDown();
- }
-
- /**
- * Create a number of credit card tokens for the test customer.
- *
- * @param int $count Number of tokens to create.
- */
- private function create_tokens_for_user( int $count ): void {
- for ( $i = 0; $i < $count; $i++ ) {
- WC_Helper_Payment_Token::create_cc_token( $this->user_id );
- }
- }
-
- /**
- * @testdox get_customer_tokens should not be limited by the posts_per_page option (issue 25025).
- */
- public function test_get_customer_tokens_is_not_limited_by_posts_per_page(): void {
- update_option( 'posts_per_page', 1 );
- $this->create_tokens_for_user( 3 );
-
- $this->assertCount(
- 3,
- WC_Payment_Tokens::get_customer_tokens( $this->user_id ),
- 'All customer tokens should be returned regardless of the posts_per_page option'
- );
- }
-
- /**
- * @testdox Data store get_tokens should not consult the posts_per_page option even when it is empty.
- */
- public function test_data_store_get_tokens_ignores_empty_posts_per_page(): void {
- add_filter( 'pre_option_posts_per_page', '__return_empty_string' );
- $this->create_tokens_for_user( 3 );
-
- $data_store = WC_Data_Store::load( 'payment-token' );
-
- // Regression guard: the data store used to fall back to posts_per_page, and an empty value produced LIMIT 0 (zero rows).
- $this->assertCount(
- 3,
- $data_store->get_tokens( array( 'user_id' => $this->user_id ) ),
- 'Token queries must not be affected by the posts_per_page option'
- );
- }
-
- /**
- * @testdox get_customer_tokens should respect the woocommerce_get_customer_payment_tokens_limit filter.
- */
- public function test_get_customer_tokens_limit_filter_is_respected(): void {
- add_filter(
- 'woocommerce_get_customer_payment_tokens_limit',
- function () {
- return 2;
- }
- );
- $this->create_tokens_for_user( 3 );
-
- $this->assertCount(
- 2,
- WC_Payment_Tokens::get_customer_tokens( $this->user_id ),
- 'The filter should still cap the number of returned tokens'
- );
- }
-
- /**
- * @testdox Data store get_tokens should return all tokens when no limit argument is passed.
- */
- public function test_data_store_get_tokens_returns_all_tokens_without_limit(): void {
- update_option( 'posts_per_page', 1 );
- $this->create_tokens_for_user( 3 );
-
- $data_store = WC_Data_Store::load( 'payment-token' );
-
- $this->assertCount(
- 3,
- $data_store->get_tokens( array( 'user_id' => $this->user_id ) ),
- 'Without an explicit limit, the data store should return all matching tokens (GDPR eraser and user-deletion cleanup rely on this)'
- );
- }
-
- /**
- * @testdox Data store get_tokens should return all tokens when page is passed without a limit.
- */
- public function test_data_store_get_tokens_ignores_page_without_limit(): void {
- $this->create_tokens_for_user( 3 );
-
- $data_store = WC_Data_Store::load( 'payment-token' );
-
- $this->assertCount(
- 3,
- $data_store->get_tokens(
- array(
- 'user_id' => $this->user_id,
- 'page' => 2,
- )
- ),
- 'A page argument without an explicit limit should not paginate the results'
- );
- }
-
- /**
- * @testdox wc_delete_user_data should delete all payment tokens, not just the customer-facing limited subset.
- */
- public function test_wc_delete_user_data_deletes_all_tokens(): void {
- add_filter(
- 'woocommerce_get_customer_payment_tokens_limit',
- function () {
- return 1;
- }
- );
- $this->create_tokens_for_user( 3 );
-
- wc_delete_user_data( $this->user_id );
-
- global $wpdb;
- $remaining = (int) $wpdb->get_var(
- $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}woocommerce_payment_tokens WHERE user_id = %d", $this->user_id )
- );
- $this->assertSame( 0, $remaining, 'Deleting a user must remove every saved payment token' );
- }
-
- /**
- * @testdox Data store get_tokens should treat page zero as the first page.
- */
- public function test_data_store_get_tokens_treats_page_zero_as_first_page(): void {
- $this->create_tokens_for_user( 3 );
-
- $data_store = WC_Data_Store::load( 'payment-token' );
-
- $this->assertCount(
- 2,
- $data_store->get_tokens(
- array(
- 'user_id' => $this->user_id,
- 'limit' => 2,
- 'page' => 0,
- )
- ),
- 'Page 0 must return the first page of results, not skip past it'
- );
- }
-
- /**
- * @testdox get_customer_tokens should default the limit filter to DEFAULT_CUSTOMER_TOKENS_LIMIT.
- */
- public function test_get_customer_tokens_defaults_to_the_documented_limit(): void {
- $received = null;
- add_filter(
- 'woocommerce_get_customer_payment_tokens_limit',
- function ( $limit ) use ( &$received ) {
- $received = $limit;
- return $limit;
- }
- );
-
- WC_Payment_Tokens::get_customer_tokens( $this->user_id );
-
- // Pins the default without creating 100 tokens: a silent change to the constant fails here.
- $this->assertSame(
- WC_Payment_Tokens::DEFAULT_CUSTOMER_TOKENS_LIMIT,
- $received,
- 'The customer token limit filter should receive DEFAULT_CUSTOMER_TOKENS_LIMIT as its default'
- );
- $this->assertSame( 100, WC_Payment_Tokens::DEFAULT_CUSTOMER_TOKENS_LIMIT );
- }
-
- /**
- * @testdox Data store get_tokens should cap an unscoped query with the fallback ceiling.
- */
- public function test_data_store_get_tokens_caps_unscoped_queries(): void {
- $received = null;
- add_filter(
- 'woocommerce_get_payment_tokens_unscoped_limit',
- function ( $limit ) use ( &$received ) {
- $received = $limit;
- return 2;
- }
- );
- $this->create_tokens_for_user( 3 );
-
- $data_store = WC_Data_Store::load( 'payment-token' );
-
- // An unscoped query matches on the unindexed gateway_id/type columns, so it must not read the
- // whole table.
- $this->assertCount(
- 2,
- $data_store->get_tokens( array() ),
- 'An unscoped query should be capped by the fallback ceiling'
- );
-
- // Pins the ceiling's default without creating 500 tokens.
- $this->assertSame(
- WC_Payment_Token_Data_Store::DEFAULT_UNSCOPED_TOKENS_LIMIT,
- $received,
- 'The unscoped limit filter should receive DEFAULT_UNSCOPED_TOKENS_LIMIT as its default'
- );
- $this->assertSame( 500, WC_Payment_Token_Data_Store::DEFAULT_UNSCOPED_TOKENS_LIMIT );
- }
-
- /**
- * @testdox Data store get_tokens should not apply the unscoped ceiling to scoped queries.
- */
- public function test_data_store_get_tokens_ceiling_does_not_apply_to_scoped_queries(): void {
- add_filter(
- 'woocommerce_get_payment_tokens_unscoped_limit',
- function () {
- return 1;
- }
- );
- $this->create_tokens_for_user( 3 );
-
- $data_store = WC_Data_Store::load( 'payment-token' );
- $token_ids = wp_list_pluck( $data_store->get_tokens( array( 'user_id' => $this->user_id ) ), 'token_id' );
-
- // The eraser and user-deletion cleanup scope by user_id and must stay unlimited.
- $this->assertCount(
- 3,
- $data_store->get_tokens( array( 'user_id' => $this->user_id ) ),
- 'A user_id-scoped query must not be capped by the unscoped ceiling'
- );
- $this->assertCount(
- 3,
- $data_store->get_tokens( array( 'token_id' => $token_ids ) ),
- 'A token_id-scoped query must not be capped by the unscoped ceiling'
- );
- }
-
- /**
- * @testdox An explicit limit should override the unscoped ceiling.
- */
- public function test_data_store_get_tokens_explicit_limit_overrides_unscoped_ceiling(): void {
- add_filter(
- 'woocommerce_get_payment_tokens_unscoped_limit',
- function () {
- return 1;
- }
- );
- $this->create_tokens_for_user( 3 );
-
- $data_store = WC_Data_Store::load( 'payment-token' );
-
- $this->assertCount(
- 3,
- $data_store->get_tokens( array( 'limit' => 10 ) ),
- 'An explicit limit should take precedence over the unscoped fallback ceiling'
- );
- }
-
- /**
- * @testdox Data store get_tokens should respect an explicit limit and page.
- */
- public function test_data_store_get_tokens_respects_explicit_limit_and_page(): void {
- $this->create_tokens_for_user( 3 );
-
- $data_store = WC_Data_Store::load( 'payment-token' );
-
- $this->assertCount(
- 2,
- $data_store->get_tokens(
- array(
- 'user_id' => $this->user_id,
- 'limit' => 2,
- )
- ),
- 'An explicit limit should cap the results'
- );
- $this->assertCount(
- 1,
- $data_store->get_tokens(
- array(
- 'user_id' => $this->user_id,
- 'limit' => 2,
- 'page' => 2,
- )
- ),
- 'Pagination should return the remaining tokens on the second page'
- );
- }
-}