From bff59757b6b10a3414d93765e082efd4e7c0cce0 Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 13:24:27 -0600 Subject: [PATCH 01/15] feat(access-control): group subscription human-readable names --- .../class-group-subscription-settings.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 95a5dea1f5..7bcec7fd45 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -19,6 +19,7 @@ class Group_Subscription_Settings { const DEFAULT_SETTINGS = [ 'enabled' => false, 'limit' => 0, + 'name' => '', ]; /** @@ -185,6 +186,13 @@ public static function get_subscription_settings( $subscription ) { $settings = self::get_product_settings( $product_id ); $settings['enabled'] = $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', true ) ? \wc_string_to_bool( $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', true ) ) : $settings['enabled']; $settings['limit'] = (int) $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'limit', true ) ?: $settings['limit']; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found + $settings['name'] = $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'name', true ) ? + $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'name', true ) : + sprintf( + /* translators: %s: The subscription owner's name. */ + __( '%s’s Group', 'newspack-plugin' ), + $subscription->get_formatted_billing_full_name() + ); /** * Filter the group subscription settings for a subscription. @@ -290,6 +298,22 @@ public static function add_group_subscription_options( $subscription ) {

+
+ self::GROUP_SUBSCRIPTION_META_PREFIX . 'name', + 'name' => self::GROUP_SUBSCRIPTION_META_PREFIX . 'name', + 'label' => __( 'Group subscription name', 'newspack-plugin' ), + 'value' => $settings['name'], + 'type' => 'text', + 'wrapper_class' => 'show_if_newspack_group_subscription_enabled', + ] + ) + ); + ?> +
$is_enabled, 'limit' => $limit, + 'name' => $name, ] ); } From c105dd377ab377bde75b3a1ff7a7e8f8e10d0fb4 Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 14:34:52 -0600 Subject: [PATCH 02/15] test(access-control): add unit tests for group subscription name setting Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/mocks/wc-mocks.php | 6 ++ .../content-gate/group-subscriptions.php | 101 +++++++++++++++++- 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/tests/mocks/wc-mocks.php b/tests/mocks/wc-mocks.php index 254685bd25..8ff66441a2 100644 --- a/tests/mocks/wc-mocks.php +++ b/tests/mocks/wc-mocks.php @@ -343,6 +343,12 @@ public function update_dates( $dates ) { $this->data['dates'][ $type ] = $date; } } + public function get_formatted_billing_full_name() { + $first = $this->data['billing_first_name'] ?? ''; + $last = $this->data['billing_last_name'] ?? ''; + $name = trim( "$first $last" ); + return $name ? $name : ''; + } public function get_items() { return $this->data['items'] ?? []; } diff --git a/tests/unit-tests/content-gate/group-subscriptions.php b/tests/unit-tests/content-gate/group-subscriptions.php index a4570797c9..3ecb7e152d 100644 --- a/tests/unit-tests/content-gate/group-subscriptions.php +++ b/tests/unit-tests/content-gate/group-subscriptions.php @@ -49,7 +49,7 @@ public function tear_down() { * * @param int $customer_id Customer/owner user ID. * @param array $settings Optional group-subscription settings to apply. - * Supported keys: 'enabled' (bool), 'limit' (int). + * Supported keys: 'enabled' (bool), 'limit' (int), 'name' (string). * @return \WC_Subscription */ private function create_group_subscription( $customer_id, $settings = [] ) { @@ -57,6 +57,7 @@ private function create_group_subscription( $customer_id, $settings = [] ) { [ 'enabled' => true, 'limit' => 0, + 'name' => '', ], $settings ); @@ -75,6 +76,9 @@ private function create_group_subscription( $customer_id, $settings = [] ) { if ( $settings['limit'] > 0 ) { $sub->update_meta_data( Group_Subscription_Settings::GROUP_SUBSCRIPTION_META_PREFIX . 'limit', $settings['limit'] ); } + if ( ! empty( $settings['name'] ) ) { + $sub->update_meta_data( Group_Subscription_Settings::GROUP_SUBSCRIPTION_META_PREFIX . 'name', $settings['name'] ); + } return $sub; } @@ -425,6 +429,101 @@ public function test_get_group_subscriptions_for_user_with_no_memberships() { $this->assertEmpty( $result ); } + // ------------------------------------------------------------------------- + // Group_Subscription_Settings name tests + // ------------------------------------------------------------------------- + + /** + * Test get_subscription_settings() returns a default name based on the + * subscription owner's billing name when no custom name is set. + */ + public function test_group_name_defaults_to_owner_name() { + $owner_id = $this->create_reader_user(); + $group_sub = $this->create_group_subscription( $owner_id ); + + // Set billing name on the subscription so get_formatted_billing_full_name() returns a real name. + $group_sub->data['billing_first_name'] = 'Jane'; + $group_sub->data['billing_last_name'] = 'Doe'; + + $settings = Group_Subscription_Settings::get_subscription_settings( $group_sub ); + + $this->assertNotEmpty( $settings['name'], 'Default name should not be empty' ); + $this->assertStringContainsString( + 'Jane', + $settings['name'], + 'Default name should contain the owner first name' + ); + $this->assertStringContainsString( + "\u{2019}s Group", + $settings['name'], + 'Default name should end with the possessive Group suffix' + ); + } + + /** + * Test get_subscription_settings() returns a custom name when one is stored + * in subscription meta. + */ + public function test_group_name_custom_override() { + $owner_id = $this->create_reader_user(); + $group_sub = $this->create_group_subscription( $owner_id, [ 'name' => 'Acme Newsroom' ] ); + + $settings = Group_Subscription_Settings::get_subscription_settings( $group_sub ); + + $this->assertEquals( 'Acme Newsroom', $settings['name'] ); + } + + /** + * Test update_subscription_settings() persists a name change that is + * reflected in subsequent get_subscription_settings() calls. + */ + public function test_group_name_update_and_read_back() { + $owner_id = $this->create_reader_user(); + $group_sub = $this->create_group_subscription( $owner_id ); + + Group_Subscription_Settings::update_subscription_settings( + $group_sub, + [ 'name' => 'Daily Planet' ] + ); + + $settings = Group_Subscription_Settings::get_subscription_settings( $group_sub ); + + $this->assertEquals( 'Daily Planet', $settings['name'] ); + } + + /** + * Test that an empty name in update_subscription_settings() stores an + * empty string, which causes get_subscription_settings() to fall back + * to the default owner-based name. + */ + public function test_group_name_empty_falls_back_to_default() { + $owner_id = $this->create_reader_user(); + $group_sub = $this->create_group_subscription( $owner_id, [ 'name' => 'Temp Name' ] ); + + // Set billing name on the subscription so the fallback name is based on the actual owner name. + $group_sub->data['billing_first_name'] = 'Jane'; + $group_sub->data['billing_last_name'] = 'Doe'; + + Group_Subscription_Settings::update_subscription_settings( + $group_sub, + [ 'name' => '' ] + ); + + $settings = Group_Subscription_Settings::get_subscription_settings( $group_sub ); + + // After clearing the custom name, it should revert to the default. + $this->assertStringContainsString( + 'Jane', + $settings['name'], + 'Clearing the name should revert to the default owner-based name' + ); + $this->assertStringContainsString( + "\u{2019}s Group", + $settings['name'], + 'Default name should end with the possessive Group suffix' + ); + } + // ------------------------------------------------------------------------- // Group_Subscription_Invite tests // ------------------------------------------------------------------------- From ee3a21a8315f3b6b9e0d04385e0091104767f251 Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 14:37:34 -0600 Subject: [PATCH 03/15] feat(access-control): show group name and member count in subscription list column Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 7bcec7fd45..9c647b2d93 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -40,6 +40,9 @@ public static function init() { \add_action( 'add_meta_boxes', [ __CLASS__, 'add_group_subscription_meta_box' ], 26, 2 ); \add_action( 'woocommerce_process_shop_order_meta', [ __CLASS__, 'save_group_subscription_meta' ], 10, 2 ); \add_action( 'wp_ajax_newspack_group_subscription_search_users', [ __CLASS__, 'ajax_search_users' ] ); + + // Customize subscription column in admin list table for group subscriptions. + \add_filter( 'woocommerce_subscription_list_table_column_content', [ __CLASS__, 'filter_subscription_column_content' ], 10, 3 ); } /** @@ -140,6 +143,43 @@ public static function add_custom_product_pricing_options( $custom_product_prici return $custom_product_pricing_options; } + /** + * Filter the subscription list table column content to show group name + * and member count for group subscriptions. + * + * @param string $column_content The column content HTML. + * @param \WC_Subscription $subscription The subscription object. + * @param string $column The column name. + * + * @return string The filtered column content. + */ + public static function filter_subscription_column_content( $column_content, $subscription, $column ) { + if ( 'order_title' !== $column ) { + return $column_content; + } + if ( ! Group_Subscription::is_group_subscription( $subscription ) ) { + return $column_content; + } + $settings = self::get_subscription_settings( $subscription ); + $members = Group_Subscription::get_members( $subscription ); + $member_count = count( $members ); + $limit = $settings['limit'] > 0 + ? $settings['limit'] + : __( 'unlimited', 'newspack-plugin' ); + + return sprintf( + '%s (%s)', + \esc_url( $subscription->get_edit_order_url() ), + \esc_html( $settings['name'] ), + sprintf( + /* translators: 1: member count, 2: member limit or "unlimited" */ + __( '%1$s of %2$s members', 'newspack-plugin' ), + $member_count, + $limit + ) + ); + } + /** * Get the group subscription settings for a product. * From a16b7025a7e67be42869aec376840eb4b696c6b7 Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 14:40:34 -0600 Subject: [PATCH 04/15] feat(access-control): add group subscription filter to subscription list view Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 9c647b2d93..a9dff43bdd 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -43,6 +43,14 @@ public static function init() { // Customize subscription column in admin list table for group subscriptions. \add_filter( 'woocommerce_subscription_list_table_column_content', [ __CLASS__, 'filter_subscription_column_content' ], 10, 3 ); + + // Group subscription filter dropdown on subscription list table. + \add_action( 'woocommerce_order_list_table_restrict_manage_orders', [ __CLASS__, 'add_group_subscription_filter' ] ); + \add_action( 'restrict_manage_posts', [ __CLASS__, 'add_group_subscription_filter' ] ); + + // Filter subscription list table query by group status. + \add_filter( 'woocommerce_shop_subscription_list_table_prepare_items_query_args', [ __CLASS__, 'filter_subscriptions_by_group' ] ); + \add_filter( 'pre_get_posts', [ __CLASS__, 'filter_subscriptions_by_group_legacy' ] ); } /** @@ -462,5 +470,152 @@ public static function save_group_subscription_meta( $subscription_id, $subscrip ] ); } + + /** + * Add a group subscription filter dropdown to the subscription list table. + * + * @param string $order_type The order type (post type or HPOS order type). + */ + public static function add_group_subscription_filter( $order_type = '' ) { + if ( '' === $order_type ) { + $order_type = isset( $GLOBALS['typenow'] ) ? $GLOBALS['typenow'] : ''; + } + + if ( 'shop_subscription' !== $order_type ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $selected = isset( $_GET['_newspack_group_subscription'] ) ? \sanitize_text_field( \wp_unslash( $_GET['_newspack_group_subscription'] ) ) : ''; + + ?> + + 'shop_subscription', + 'status' => 'any', + 'limit' => -1, + 'return' => 'ids', + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', + 'value' => 'yes', + ], + ], + ] + ); + } + + // 2. Subscription IDs that have at least one group member. + $member_sub_ids = array_map( + 'absint', + $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT DISTINCT meta_value FROM {$wpdb->usermeta} WHERE meta_key = %s", + Group_Subscription::GROUP_SUBSCRIPTION_USER_META_KEY + ) + ) + ); + + return array_values( array_unique( array_merge( $enabled_ids, $member_sub_ids ) ) ); + } + + /** + * Filter the subscription list table query by group subscription status (HPOS). + * + * @param array $query_args The query args for the list table. + * + * @return array The filtered query args. + */ + public static function filter_subscriptions_by_group( $query_args ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( empty( $_GET['_newspack_group_subscription'] ) ) { + return $query_args; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $filter = \sanitize_text_field( \wp_unslash( $_GET['_newspack_group_subscription'] ) ); + if ( ! in_array( $filter, [ 'group', 'non-group' ], true ) ) { + return $query_args; + } + + $group_ids = self::get_group_subscription_ids(); + + if ( 'group' === $filter ) { + if ( empty( $group_ids ) ) { + $query_args['post__in'] = [ 0 ]; + } elseif ( ! isset( $query_args['post__in'] ) ) { + $query_args['post__in'] = $group_ids; + } else { + $intersected = array_intersect( $query_args['post__in'], $group_ids ); + $query_args['post__in'] = empty( $intersected ) ? [ 0 ] : array_values( $intersected ); + } + } elseif ( 'non-group' === $filter ) { + if ( ! empty( $group_ids ) ) { + $query_args['post__not_in'] = isset( $query_args['post__not_in'] ) // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in + ? array_merge( $query_args['post__not_in'], $group_ids ) + : $group_ids; + } + } + + return $query_args; + } + + /** + * Filter the subscription list by group subscription status (legacy CPT). + * + * @param \WP_Query $query The WP_Query instance. + */ + public static function filter_subscriptions_by_group_legacy( $query ) { // phpcs:ignore WordPressVIPMinimum.Hooks.AlwaysReturnInFilter.VoidReturn, WordPressVIPMinimum.Hooks.AlwaysReturnInFilter.MissingReturnStatement + if ( ! is_admin() || ! $query->is_main_query() ) { + return; + } + + if ( 'shop_subscription' !== $query->get( 'post_type' ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( empty( $_GET['_newspack_group_subscription'] ) ) { + return; + } + + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $filter = \sanitize_text_field( \wp_unslash( $_GET['_newspack_group_subscription'] ) ); + if ( ! in_array( $filter, [ 'group', 'non-group' ], true ) ) { + return; + } + + $group_ids = self::get_group_subscription_ids(); + + if ( 'group' === $filter ) { + $query->set( 'post__in', empty( $group_ids ) ? [ 0 ] : $group_ids ); + } elseif ( 'non-group' === $filter && ! empty( $group_ids ) ) { + $existing_not_in = $query->get( 'post__not_in' ); + $query->set( 'post__not_in', array_merge( ! empty( $existing_not_in ) ? $existing_not_in : [], $group_ids ) ); + } + } } Group_Subscription_Settings::init(); From 44ab5356fa3008fad7eee5eb867b519b8bd86c2c Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 14:54:21 -0600 Subject: [PATCH 05/15] fix(access-control): escape group subscription column output Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index a9dff43bdd..b116d90b34 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -179,11 +179,13 @@ public static function filter_subscription_column_content( $column_content, $sub '%s (%s)', \esc_url( $subscription->get_edit_order_url() ), \esc_html( $settings['name'] ), - sprintf( - /* translators: 1: member count, 2: member limit or "unlimited" */ - __( '%1$s of %2$s members', 'newspack-plugin' ), - $member_count, - $limit + \esc_html( + sprintf( + /* translators: 1: member count, 2: member limit or "unlimited" */ + __( '%1$s of %2$s members', 'newspack-plugin' ), + $member_count, + $limit + ) ) ); } From 67a1802f9c11abc9622429986b751c876173b690 Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 15:24:03 -0600 Subject: [PATCH 06/15] fix(access-control): simplify group filter to only match subscriptions with members Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 29 ++----------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index b116d90b34..e70c67a1c5 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -502,36 +502,15 @@ public static function add_group_subscription_filter( $order_type = '' ) { /** * Get all subscription IDs that are group subscriptions. * - * Collects IDs from two sources: - * 1. Subscriptions with the group enabled meta set to 'yes'. - * 2. Subscriptions that have at least one group member (via user meta). + * Returns subscription IDs that have at least one group member, + * based on user meta associations. * * @return int[] Array of subscription IDs. */ public static function get_group_subscription_ids() { global $wpdb; - // 1. Subscription IDs with group enabled meta. - $enabled_ids = []; - if ( function_exists( 'wc_get_orders' ) ) { - $enabled_ids = \wc_get_orders( - [ - 'type' => 'shop_subscription', - 'status' => 'any', - 'limit' => -1, - 'return' => 'ids', - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - [ - 'key' => self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', - 'value' => 'yes', - ], - ], - ] - ); - } - - // 2. Subscription IDs that have at least one group member. - $member_sub_ids = array_map( + return array_map( 'absint', $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( @@ -540,8 +519,6 @@ public static function get_group_subscription_ids() { ) ) ); - - return array_values( array_unique( array_merge( $enabled_ids, $member_sub_ids ) ) ); } /** From 3788912042f063ab21bbfba8b557c298cd5dc72d Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 15:54:10 -0600 Subject: [PATCH 07/15] feat(access-control): include product-inherited group subscriptions in filter Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 67 +++++++++++++++---- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index e70c67a1c5..a8a4df9f58 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -233,6 +233,7 @@ public static function get_subscription_settings( $subscription ) { return self::DEFAULT_SETTINGS; } $product_id = WooCommerce_Subscriptions::get_subscription_product_id( $subscription ); + $owner_name = trim( $subscription->get_formatted_billing_full_name() ); $settings = self::get_product_settings( $product_id ); $settings['enabled'] = $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', true ) ? \wc_string_to_bool( $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', true ) ) : $settings['enabled']; $settings['limit'] = (int) $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'limit', true ) ?: $settings['limit']; // phpcs:ignore Universal.Operators.DisallowShortTernary.Found @@ -240,8 +241,8 @@ public static function get_subscription_settings( $subscription ) { $subscription->get_meta( self::GROUP_SUBSCRIPTION_META_PREFIX . 'name', true ) : sprintf( /* translators: %s: The subscription owner's name. */ - __( '%s’s Group', 'newspack-plugin' ), - $subscription->get_formatted_billing_full_name() + __( '%s Group', 'newspack-plugin' ), + $owner_name ? $owner_name . '’s' : __( 'Unnamed', 'newspack-plugin' ) ); /** @@ -502,23 +503,65 @@ public static function add_group_subscription_filter( $order_type = '' ) { /** * Get all subscription IDs that are group subscriptions. * - * Returns subscription IDs that have at least one group member, - * based on user meta associations. + * Collects IDs from two sources: + * 1. Subscriptions with the group enabled meta set directly. + * 2. Subscriptions whose product has group subscriptions enabled (inheritance). * * @return int[] Array of subscription IDs. */ public static function get_group_subscription_ids() { global $wpdb; - return array_map( - 'absint', - $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->prepare( - "SELECT DISTINCT meta_value FROM {$wpdb->usermeta} WHERE meta_key = %s", - Group_Subscription::GROUP_SUBSCRIPTION_USER_META_KEY - ) - ) + // 1. Subscription IDs with group enabled meta set directly. + $enabled_ids = []; + if ( function_exists( 'wc_get_orders' ) ) { + $enabled_ids = \wc_get_orders( + [ + 'type' => 'shop_subscription', + 'status' => 'any', + 'limit' => -1, + 'return' => 'ids', + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', + 'value' => 'yes', + ], + ], + ] + ); + } + + // 2. Subscription IDs whose product has group subscriptions enabled. + $product_ids = \get_posts( // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.get_posts_get_posts + [ + 'post_type' => [ 'product', 'product_variation' ], + 'posts_per_page' => -1, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page + 'fields' => 'ids', + 'meta_key' => self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => 'yes', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ] ); + + $product_sub_ids = []; + if ( ! empty( $product_ids ) ) { + $placeholders = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) ); + $product_sub_ids = array_map( + 'absint', + $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching + $wpdb->prepare( + "SELECT DISTINCT oi.order_id + FROM {$wpdb->prefix}woocommerce_order_items oi + INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim + ON oi.order_item_id = oim.order_item_id + AND oim.meta_key = '_product_id' + WHERE oim.meta_value IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared + ...$product_ids + ) + ) + ); + } + + return array_values( array_unique( array_merge( $enabled_ids, $product_sub_ids ) ) ); } /** From 9fb2b61445e38d33fd35149ed709599840d80664 Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 16:00:22 -0600 Subject: [PATCH 08/15] feat(access-control): include group name in subscription search Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index a8a4df9f58..8bf559ddde 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -51,6 +51,9 @@ public static function init() { // Filter subscription list table query by group status. \add_filter( 'woocommerce_shop_subscription_list_table_prepare_items_query_args', [ __CLASS__, 'filter_subscriptions_by_group' ] ); \add_filter( 'pre_get_posts', [ __CLASS__, 'filter_subscriptions_by_group_legacy' ] ); + + // Include group name in subscription search. + \add_filter( 'woocommerce_shop_subscription_search_fields', [ __CLASS__, 'add_group_name_search_field' ] ); } /** @@ -500,6 +503,18 @@ public static function add_group_subscription_filter( $order_type = '' ) { Date: Wed, 15 Apr 2026 16:06:02 -0600 Subject: [PATCH 09/15] feat(access-control): include group name in WP_Query subscription search Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 8bf559ddde..085f09d9e4 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -54,6 +54,8 @@ public static function init() { // Include group name in subscription search. \add_filter( 'woocommerce_shop_subscription_search_fields', [ __CLASS__, 'add_group_name_search_field' ] ); + \add_filter( 'posts_join', [ __CLASS__, 'search_group_name_join' ], 10, 2 ); + \add_filter( 'posts_search', [ __CLASS__, 'search_group_name_where' ], 10, 2 ); } /** @@ -515,6 +517,59 @@ public static function add_group_name_search_field( $search_fields ) { return $search_fields; } + /** + * Join the postmeta table for group name search in WP_Query. + * + * @param string $join The JOIN clause. + * @param \WP_Query $query The WP_Query instance. + * + * @return string The modified JOIN clause. + */ + public static function search_group_name_join( $join, $query ) { + global $wpdb; + if ( ! self::is_subscription_search_query( $query ) ) { + return $join; + } + $join .= " LEFT JOIN {$wpdb->postmeta} AS np_group_name ON ( {$wpdb->posts}.ID = np_group_name.post_id AND np_group_name.meta_key = '" . esc_sql( self::GROUP_SUBSCRIPTION_META_PREFIX . 'name' ) . "' ) "; + return $join; + } + + /** + * Extend the search WHERE clause to include group name meta in WP_Query. + * + * @param string $search The search WHERE clause. + * @param \WP_Query $query The WP_Query instance. + * + * @return string The modified search WHERE clause. + */ + public static function search_group_name_where( $search, $query ) { + global $wpdb; + if ( ! self::is_subscription_search_query( $query ) || empty( $search ) ) { + return $search; + } + $term = $query->get( 's' ); + if ( empty( $term ) ) { + return $search; + } + $like = '%' . $wpdb->esc_like( $term ) . '%'; + $or_clause = $wpdb->prepare( ' OR ( np_group_name.meta_value LIKE %s )', $like ); + + // Insert the OR clause before the closing parenthesis of the search condition. + $search = preg_replace( '/\)\s*$/', $or_clause . ' )', $search ); + return $search; + } + + /** + * Check if a WP_Query is a search query for shop_subscription post type. + * + * @param \WP_Query $query The WP_Query instance. + * + * @return bool Whether this is a subscription search query. + */ + private static function is_subscription_search_query( $query ) { + return $query->is_search() && 'shop_subscription' === $query->get( 'post_type' ); + } + /** * Get all subscription IDs that are group subscriptions. * From 9bbe0568925ca9c9e5047fd5988d65b1ef532ac0 Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 16:17:21 -0600 Subject: [PATCH 10/15] fix(access-control): include group name in HPOS subscription search Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 085f09d9e4..271a73fe7a 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -54,6 +54,7 @@ public static function init() { // Include group name in subscription search. \add_filter( 'woocommerce_shop_subscription_search_fields', [ __CLASS__, 'add_group_name_search_field' ] ); + \add_filter( 'woocommerce_order_table_search_query_meta_keys', [ __CLASS__, 'add_group_name_hpos_search_field' ] ); \add_filter( 'posts_join', [ __CLASS__, 'search_group_name_join' ], 10, 2 ); \add_filter( 'posts_search', [ __CLASS__, 'search_group_name_where' ], 10, 2 ); } @@ -517,6 +518,29 @@ public static function add_group_name_search_field( $search_fields ) { return $search_fields; } + /** + * Add the group subscription name meta key to the HPOS order search meta keys. + * + * This filter fires for all order types, so we guard it to only apply + * on the subscription admin list table screen. + * + * @param array $meta_keys The meta keys to search. + * + * @return array The meta keys with the group name meta key added. + */ + public static function add_group_name_hpos_search_field( $meta_keys ) { + if ( ! is_admin() ) { + return $meta_keys; + } + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? \sanitize_text_field( \wp_unslash( $_GET['page'] ) ) : ''; + if ( 'wc-orders--shop_subscription' !== $page ) { + return $meta_keys; + } + $meta_keys[] = self::GROUP_SUBSCRIPTION_META_PREFIX . 'name'; + return $meta_keys; + } + /** * Join the postmeta table for group name search in WP_Query. * From 14f848c92996da74b026acaae9e52bd9e91e169b Mon Sep 17 00:00:00 2001 From: dkoo Date: Wed, 15 Apr 2026 17:03:55 -0600 Subject: [PATCH 11/15] fix(access-control): address PR review feedback for group subscription admin - Fix search WHERE clause regex to handle nested parentheses - Scope order items query to shop_subscription type only - Cache group subscription IDs in a 5-minute transient - Invalidate cache on subscription and product setting changes Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 80 +++++++++++++++++-- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 271a73fe7a..12eceb263c 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -52,6 +52,9 @@ public static function init() { \add_filter( 'woocommerce_shop_subscription_list_table_prepare_items_query_args', [ __CLASS__, 'filter_subscriptions_by_group' ] ); \add_filter( 'pre_get_posts', [ __CLASS__, 'filter_subscriptions_by_group_legacy' ] ); + // Clear group subscription IDs cache when product group settings change. + \add_action( 'woocommerce_process_product_meta', [ __CLASS__, 'maybe_clear_cache_on_product_save' ] ); + // Include group name in subscription search. \add_filter( 'woocommerce_shop_subscription_search_fields', [ __CLASS__, 'add_group_name_search_field' ] ); \add_filter( 'woocommerce_order_table_search_query_meta_keys', [ __CLASS__, 'add_group_name_hpos_search_field' ] ); @@ -290,6 +293,11 @@ public static function update_subscription_settings( $subscription, $settings ) } if ( $should_save ) { $subscription->save(); + + // Clear the cached group subscription IDs if the enabled setting changed. + if ( isset( $settings['enabled'] ) ) { + self::clear_group_subscription_ids_cache(); + } } } @@ -575,11 +583,16 @@ public static function search_group_name_where( $search, $query ) { if ( empty( $term ) ) { return $search; } - $like = '%' . $wpdb->esc_like( $term ) . '%'; + $like = '%' . $wpdb->esc_like( $term ) . '%'; $or_clause = $wpdb->prepare( ' OR ( np_group_name.meta_value LIKE %s )', $like ); - // Insert the OR clause before the closing parenthesis of the search condition. - $search = preg_replace( '/\)\s*$/', $or_clause . ' )', $search ); + // Insert the OR clause inside the existing grouped search condition. + // WP's search clause can end with )) or ) depending on search terms. + if ( preg_match( '/\)\)\s*$/', $search ) ) { + $search = preg_replace( '/\)\)\s*$/', $or_clause . ' ))', $search, 1 ); + } else { + $search = preg_replace( '/\)\s*$/', $or_clause . ' )', $search, 1 ); + } return $search; } @@ -594,6 +607,11 @@ private static function is_subscription_search_query( $query ) { return $query->is_search() && 'shop_subscription' === $query->get( 'post_type' ); } + /** + * Transient key for caching group subscription IDs. + */ + const GROUP_SUBSCRIPTION_IDS_TRANSIENT = 'newspack_group_subscription_ids'; + /** * Get all subscription IDs that are group subscriptions. * @@ -601,9 +619,16 @@ private static function is_subscription_search_query( $query ) { * 1. Subscriptions with the group enabled meta set directly. * 2. Subscriptions whose product has group subscriptions enabled (inheritance). * + * Results are cached in a transient for 5 minutes. + * * @return int[] Array of subscription IDs. */ public static function get_group_subscription_ids() { + $cached = \get_transient( self::GROUP_SUBSCRIPTION_IDS_TRANSIENT ); + if ( false !== $cached ) { + return $cached; + } + global $wpdb; // 1. Subscription IDs with group enabled meta set directly. @@ -637,9 +662,10 @@ public static function get_group_subscription_ids() { ); $product_sub_ids = []; - if ( ! empty( $product_ids ) ) { - $placeholders = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) ); - $product_sub_ids = array_map( + if ( ! empty( $product_ids ) && function_exists( 'wc_get_orders' ) ) { + // Get order IDs containing these products via order items tables. + $placeholders = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) ); + $candidate_ids = array_map( 'absint', $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->prepare( @@ -653,9 +679,49 @@ public static function get_group_subscription_ids() { ) ) ); + + // Filter to only include shop_subscription order types. + if ( ! empty( $candidate_ids ) ) { + $product_sub_ids = \wc_get_orders( + [ + 'type' => 'shop_subscription', + 'status' => 'any', + 'limit' => -1, + 'return' => 'ids', + 'post__in' => $candidate_ids, + ] + ); + } } - return array_values( array_unique( array_merge( $enabled_ids, $product_sub_ids ) ) ); + $result = array_values( array_unique( array_merge( $enabled_ids, $product_sub_ids ) ) ); + + \set_transient( self::GROUP_SUBSCRIPTION_IDS_TRANSIENT, $result, 5 * MINUTE_IN_SECONDS ); + + return $result; + } + + /** + * Clear the group subscription IDs transient cache. + * + * Called when group subscription settings change to ensure the filter + * reflects current state. + */ + public static function clear_group_subscription_ids_cache() { + \delete_transient( self::GROUP_SUBSCRIPTION_IDS_TRANSIENT ); + } + + /** + * Clear the group subscription IDs cache when a product's group settings + * may have changed. + * + * @param int $product_id The product ID being saved. + */ + public static function maybe_clear_cache_on_product_save( $product_id ) { + // phpcs:ignore WordPress.Security.NonceVerification.Missing + if ( isset( $_POST[ self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled' ] ) || \get_post_meta( $product_id, self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', true ) ) { + self::clear_group_subscription_ids_cache(); + } } /** From 6d99b7168504d61f3cc4e490d312e74974f77316 Mon Sep 17 00:00:00 2001 From: dkoo Date: Thu, 16 Apr 2026 15:33:40 -0600 Subject: [PATCH 12/15] fix(access-control): use request filter for CPT-mode group subscription filtering Replace pre_get_posts with the request filter for CPT-mode subscription list tables. This matches the approach WCS uses for its own filters and works reliably across WooCommerce versions. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 12eceb263c..3bd6a80862 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -50,7 +50,7 @@ public static function init() { // Filter subscription list table query by group status. \add_filter( 'woocommerce_shop_subscription_list_table_prepare_items_query_args', [ __CLASS__, 'filter_subscriptions_by_group' ] ); - \add_filter( 'pre_get_posts', [ __CLASS__, 'filter_subscriptions_by_group_legacy' ] ); + \add_filter( 'request', [ __CLASS__, 'filter_subscriptions_by_group_cpt' ] ); // Clear group subscription IDs cache when product group settings change. \add_action( 'woocommerce_process_product_meta', [ __CLASS__, 'maybe_clear_cache_on_product_save' ] ); @@ -766,38 +766,53 @@ public static function filter_subscriptions_by_group( $query_args ) { } /** - * Filter the subscription list by group subscription status (legacy CPT). + * Filter the subscription list by group subscription status (CPT mode). * - * @param \WP_Query $query The WP_Query instance. + * Uses the 'request' filter, which is the same approach WCS uses for its + * own product/customer/payment method filters on edit.php. + * + * @param array $query_vars The query vars for the admin list table request. + * + * @return array The filtered query vars. */ - public static function filter_subscriptions_by_group_legacy( $query ) { // phpcs:ignore WordPressVIPMinimum.Hooks.AlwaysReturnInFilter.VoidReturn, WordPressVIPMinimum.Hooks.AlwaysReturnInFilter.MissingReturnStatement - if ( ! is_admin() || ! $query->is_main_query() ) { - return; - } + public static function filter_subscriptions_by_group_cpt( $query_vars ) { + global $typenow; - if ( 'shop_subscription' !== $query->get( 'post_type' ) ) { - return; + if ( ! is_admin() || 'shop_subscription' !== $typenow ) { + return $query_vars; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( empty( $_GET['_newspack_group_subscription'] ) ) { - return; + return $query_vars; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $filter = \sanitize_text_field( \wp_unslash( $_GET['_newspack_group_subscription'] ) ); if ( ! in_array( $filter, [ 'group', 'non-group' ], true ) ) { - return; + return $query_vars; } $group_ids = self::get_group_subscription_ids(); if ( 'group' === $filter ) { - $query->set( 'post__in', empty( $group_ids ) ? [ 0 ] : $group_ids ); - } elseif ( 'non-group' === $filter && ! empty( $group_ids ) ) { - $existing_not_in = $query->get( 'post__not_in' ); - $query->set( 'post__not_in', array_merge( ! empty( $existing_not_in ) ? $existing_not_in : [], $group_ids ) ); + if ( empty( $group_ids ) ) { + $query_vars['post__in'] = [ 0 ]; + } elseif ( ! isset( $query_vars['post__in'] ) ) { + $query_vars['post__in'] = $group_ids; + } else { + $intersected = array_intersect( $query_vars['post__in'], $group_ids ); + $query_vars['post__in'] = empty( $intersected ) ? [ 0 ] : array_values( $intersected ); + } + } elseif ( 'non-group' === $filter ) { + if ( ! empty( $group_ids ) ) { + $query_vars['post__not_in'] = isset( $query_vars['post__not_in'] ) // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in + ? array_merge( $query_vars['post__not_in'], $group_ids ) + : $group_ids; + } } + + return $query_vars; } } Group_Subscription_Settings::init(); From 5e9f22fda9f6ba417115f6387f658b496e58aa35 Mon Sep 17 00:00:00 2001 From: dkoo Date: Thu, 16 Apr 2026 15:48:28 -0600 Subject: [PATCH 13/15] fix(access-control): use WCS API functions for group subscription ID queries Replace wc_get_orders (which ignores meta_query in CPT mode) with wcs_get_subscriptions and wcs_get_subscriptions_for_product, which properly handle meta queries in both CPT and HPOS storage modes. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 65 +++++++------------ 1 file changed, 22 insertions(+), 43 deletions(-) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 3bd6a80862..8069d6840b 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -629,24 +629,26 @@ public static function get_group_subscription_ids() { return $cached; } - global $wpdb; + $meta_key = self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled'; // 1. Subscription IDs with group enabled meta set directly. + // Uses wcs_get_subscriptions which properly handles meta_query in both + // CPT and HPOS modes (via wcs_get_orders_with_meta_query internally). $enabled_ids = []; - if ( function_exists( 'wc_get_orders' ) ) { - $enabled_ids = \wc_get_orders( - [ - 'type' => 'shop_subscription', - 'status' => 'any', - 'limit' => -1, - 'return' => 'ids', - 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - [ - 'key' => self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', - 'value' => 'yes', + if ( function_exists( 'wcs_get_subscriptions' ) ) { + $enabled_ids = array_keys( + \wcs_get_subscriptions( + [ + 'subscriptions_per_page' => -1, + 'subscription_status' => 'any', + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => $meta_key, + 'value' => 'yes', + ], ], - ], - ] + ] + ) ); } @@ -656,40 +658,17 @@ public static function get_group_subscription_ids() { 'post_type' => [ 'product', 'product_variation' ], 'posts_per_page' => -1, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page 'fields' => 'ids', - 'meta_key' => self::GROUP_SUBSCRIPTION_META_PREFIX . 'enabled', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_key' => $meta_key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => 'yes', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value ] ); $product_sub_ids = []; - if ( ! empty( $product_ids ) && function_exists( 'wc_get_orders' ) ) { - // Get order IDs containing these products via order items tables. - $placeholders = implode( ',', array_fill( 0, count( $product_ids ), '%d' ) ); - $candidate_ids = array_map( - 'absint', - $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching - $wpdb->prepare( - "SELECT DISTINCT oi.order_id - FROM {$wpdb->prefix}woocommerce_order_items oi - INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta oim - ON oi.order_item_id = oim.order_item_id - AND oim.meta_key = '_product_id' - WHERE oim.meta_value IN ($placeholders)", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare, WordPress.DB.PreparedSQL.NotPrepared - ...$product_ids - ) - ) - ); - - // Filter to only include shop_subscription order types. - if ( ! empty( $candidate_ids ) ) { - $product_sub_ids = \wc_get_orders( - [ - 'type' => 'shop_subscription', - 'status' => 'any', - 'limit' => -1, - 'return' => 'ids', - 'post__in' => $candidate_ids, - ] + if ( ! empty( $product_ids ) && function_exists( 'wcs_get_subscriptions_for_product' ) ) { + foreach ( $product_ids as $product_id ) { + $product_sub_ids = array_merge( + $product_sub_ids, + array_keys( \wcs_get_subscriptions_for_product( $product_id ) ) ); } } From 6acdadf3cbaada34ef053055db6d70e72b23f4ed Mon Sep 17 00:00:00 2001 From: dkoo Date: Thu, 16 Apr 2026 16:10:28 -0600 Subject: [PATCH 14/15] fix(access-control): skip variable parent products in group subscription filter Variations have their own independent group subscription settings and do not inherit from the parent variable product. Skip variable parent products when enumerating subscriptions via wcs_get_subscriptions_for_product to prevent incorrectly matching subscriptions whose variations aren't group-enabled. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 8069d6840b..9232737fc5 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -664,8 +664,17 @@ public static function get_group_subscription_ids() { ); $product_sub_ids = []; - if ( ! empty( $product_ids ) && function_exists( 'wcs_get_subscriptions_for_product' ) ) { + if ( ! empty( $product_ids ) && function_exists( 'wcs_get_subscriptions_for_product' ) && function_exists( 'wc_get_product' ) ) { foreach ( $product_ids as $product_id ) { + $product = \wc_get_product( $product_id ); + if ( ! $product ) { + continue; + } + // Skip variable parent products: variations have their own group settings + // and do not inherit from the parent. + if ( $product->is_type( [ 'variable', 'variable-subscription' ] ) ) { + continue; + } $product_sub_ids = array_merge( $product_sub_ids, array_keys( \wcs_get_subscriptions_for_product( $product_id ) ) From 5772d59f9ad2e6e0c76f932f050668eab0168ec5 Mon Sep 17 00:00:00 2001 From: dkoo Date: Thu, 16 Apr 2026 16:16:43 -0600 Subject: [PATCH 15/15] fix(access-control): respect subscription-level opt-out in group filter A subscription's own _newspack_group_subscription_enabled = 'no' meta should override the product-level 'yes' setting. Remove explicitly opted-out subscriptions from the product inheritance path. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-group-subscription-settings.php | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php index 9232737fc5..0fa9640911 100644 --- a/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php +++ b/includes/plugins/woocommerce-subscriptions/group-subscription/class-group-subscription-settings.php @@ -682,6 +682,28 @@ public static function get_group_subscription_ids() { } } + // 3. Remove subscriptions that explicitly opted out via an 'enabled = no' override. + // A subscription's own meta takes precedence over product inheritance. + $opted_out_ids = []; + if ( ! empty( $product_sub_ids ) && function_exists( 'wcs_get_subscriptions' ) ) { + $opted_out_ids = array_keys( + \wcs_get_subscriptions( + [ + 'subscriptions_per_page' => -1, + 'subscription_status' => 'any', + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => $meta_key, + 'value' => 'no', + ], + ], + ] + ) + ); + } + + $product_sub_ids = array_diff( $product_sub_ids, $opted_out_ids ); + $result = array_values( array_unique( array_merge( $enabled_ids, $product_sub_ids ) ) ); \set_transient( self::GROUP_SUBSCRIPTION_IDS_TRANSIENT, $result, 5 * MINUTE_IN_SECONDS );