From aebe581d451dc27429b40a222a9c712227fa44d9 Mon Sep 17 00:00:00 2001 From: Derrick Koo Date: Thu, 1 Aug 2024 09:57:44 -0600 Subject: [PATCH] fix: force active membership if the user has active subs (#3268) * fix: force active membership if the user has active subs * fix: match memberships product ownership logic (OR, not AND) * fix: logical error * fix: logical error * fix: incorrect condition * fix: feedback from code review --- .../wc-memberships/class-memberships.php | 96 +++++++++++++++---- .../class-woocommerce-connection.php | 49 ++++++++-- 2 files changed, 118 insertions(+), 27 deletions(-) diff --git a/includes/plugins/wc-memberships/class-memberships.php b/includes/plugins/wc-memberships/class-memberships.php index 635e43d53b..d7d8749488 100644 --- a/includes/plugins/wc-memberships/class-memberships.php +++ b/includes/plugins/wc-memberships/class-memberships.php @@ -8,6 +8,7 @@ namespace Newspack; use Newspack\Memberships\Metering; +use Newspack\WooCommerce_Connection; defined( 'ABSPATH' ) || exit; @@ -56,6 +57,7 @@ public static function init() { add_filter( 'newspack_popups_assess_has_disabled_popups', [ __CLASS__, 'disable_popups' ] ); add_filter( 'newspack_reader_activity_article_view', [ __CLASS__, 'suppress_article_view_activity' ], 100 ); add_filter( 'user_has_cap', [ __CLASS__, 'user_has_cap' ], 10, 3 ); + add_filter( 'get_post_status', [ __CLASS__, 'check_membership_status' ], 10, 2 ); add_action( 'wp', [ __CLASS__, 'remove_unnecessary_content_restriction' ], 11 ); /** Add gate content filters to mimic 'the_content'. See 'wp-includes/default-filters.php' for reference. */ @@ -78,6 +80,13 @@ public static function init() { include __DIR__ . '/class-metering.php'; } + /** + * Check if Memberships is available. + */ + public static function is_active() { + return class_exists( 'WC_Memberships' ) && function_exists( 'wc_memberships' ); + } + /** * Parses dynamic blocks out of `post_content` and re-renders them. * @@ -321,7 +330,7 @@ private static function get_plan_gate_id( $plan_id ) { * @return string[] Plan names keyed by plan ID. */ private static function get_gate_plans( $gate_id ) { - if ( ! function_exists( 'wc_memberships_get_membership_plan' ) ) { + if ( ! self::is_active() || ! function_exists( 'wc_memberships_get_membership_plan' ) ) { return []; } $ids = get_post_meta( $gate_id, 'plans', true ); @@ -344,7 +353,7 @@ private static function get_gate_plans( $gate_id ) { * @return array */ public static function get_plans() { - if ( ! function_exists( 'wc_memberships_get_membership_plans' ) ) { + if ( ! self::is_active() || ! function_exists( 'wc_memberships_get_membership_plans' ) ) { return []; } $membership_plans = wc_memberships_get_membership_plans(); @@ -415,7 +424,7 @@ private static function current_user_has_plan( $plan_id ) { if ( ! \is_user_logged_in() ) { return false; } - if ( ! function_exists( 'wc_memberships_is_user_active_or_delayed_member' ) ) { + if ( ! self::is_active() || ! function_exists( 'wc_memberships_is_user_active_or_delayed_member' ) ) { return false; } return \wc_memberships_is_user_active_or_delayed_member( \get_current_user_id(), $plan_id ); @@ -480,7 +489,7 @@ public static function is_post_restricted( $post_id = null ) { if ( ! $post_id ) { $post_id = get_the_ID(); } - if ( ! function_exists( 'wc_memberships_is_post_content_restricted' ) || ! \wc_memberships_is_post_content_restricted( $post_id ) ) { + if ( ! self::is_active() || ! function_exists( 'wc_memberships_is_post_content_restricted' ) || ! \wc_memberships_is_post_content_restricted( $post_id ) ) { return false; } return ! is_user_logged_in() || ! current_user_can( 'wc_memberships_view_restricted_post_content', $post_id ); // phpcs:ignore WordPress.WP.Capabilities.Unknown @@ -795,7 +804,7 @@ private static function can_manage_woocommerce( $caps ) { */ public static function user_has_cap( $all_caps, $caps, $args ) { // Bail if Woo Memberships is not active. - if ( ! class_exists( 'WC_Memberships' ) || ! function_exists( 'wc_memberships' ) ) { + if ( ! self::is_active() ) { return $all_caps; } @@ -803,6 +812,18 @@ public static function user_has_cap( $all_caps, $caps, $args ) { foreach ( $caps as $cap ) { switch ( $cap ) { + case 'wc_memberships_access_all_restricted_content': + case 'wc_memberships_view_restricted_product': + case 'wc_memberships_purchase_restricted_product': + case 'wc_memberships_view_restricted_product_taxonomy_term': + case 'wc_memberships_view_delayed_product_taxonomy_term': + case 'wc_memberships_view_restricted_taxonomy_term': + case 'wc_memberships_view_restricted_taxonomy': + case 'wc_memberships_view_restricted_post_type': + case 'wc_memberships_view_delayed_post_type': + case 'wc_memberships_view_delayed_taxonomy': + case 'wc_memberships_view_delayed_taxonomy_term': + case 'wc_memberships_view_delayed_post_content': case 'wc_memberships_view_restricted_post_content': if ( self::can_manage_woocommerce( $all_caps ) ) { $all_caps[ $cap ] = true; @@ -815,6 +836,10 @@ public static function user_has_cap( $all_caps, $caps, $args ) { break; } + if ( ! isset( $args[1] ) || ! isset( $args[2] ) ) { + break; + } + $user_id = (int) $args[1]; $post_id = (int) $args[2]; @@ -828,20 +853,8 @@ public static function user_has_cap( $all_caps, $caps, $args ) { break; - case 'wc_memberships_access_all_restricted_content': - case 'wc_memberships_view_restricted_product': - case 'wc_memberships_purchase_restricted_product': - case 'wc_memberships_view_restricted_product_taxonomy_term': - case 'wc_memberships_view_delayed_product_taxonomy_term': - case 'wc_memberships_view_restricted_taxonomy_term': - case 'wc_memberships_view_restricted_taxonomy': - case 'wc_memberships_view_restricted_post_type': - case 'wc_memberships_view_delayed_post_type': - case 'wc_memberships_view_delayed_taxonomy': - case 'wc_memberships_view_delayed_taxonomy_term': - case 'wc_memberships_view_delayed_post_content': case 'wc_memberships_view_delayed_product': - // Allow user who can edit posts (by default: editors, authors, contributors). + // Allow users who can edit posts (by default: editors, authors, contributors). if ( isset( $all_caps['edit_posts'] ) && true === $all_caps['edit_posts'] ) { $all_caps[ $cap ] = true; break; @@ -876,10 +889,19 @@ private static function user_has_content_access_from_rules( $user_id, array $rul return true; } + $integrations = wc_memberships()->get_integrations_instance(); + $integration = $integrations ? $integrations->get_subscriptions_instance() : null; $require_all_plans = self::get_require_all_plans_setting(); $has_access = false; + $has_subscription = false; foreach ( $rules as $rule ) { + $membership_plan_id = $rule->get_membership_plan_id(); + if ( $integration && $integration->has_membership_plan_subscription( $membership_plan_id ) ) { + $subscription_plan = new \WC_Memberships_Integration_Subscriptions_Membership_Plan( $membership_plan_id ); + $required_products = $subscription_plan->get_subscription_product_ids(); + $has_subscription = ! empty( WooCommerce_Connection::get_active_subscriptions_for_user( $user_id, $required_products ) ); + } // If no object ID is provided, then we are looking at rules that apply to whole post types or taxonomies. // In this case, rules that apply to specific objects should be skipped. @@ -887,7 +909,7 @@ private static function user_has_content_access_from_rules( $user_id, array $rul continue; } - if ( wc_memberships_is_user_active_or_delayed_member( $user_id, $rule->get_membership_plan_id() ) ) { + if ( $has_subscription || wc_memberships_is_user_active_or_delayed_member( $user_id, $rule->get_membership_plan_id() ) ) { $has_access = true; if ( ! $require_all_plans ) { break; @@ -901,6 +923,41 @@ private static function user_has_content_access_from_rules( $user_id, array $rul return $has_access; } + /** + * Check if a user has an active subscription with the required products when checking membership status. + * If they have an active subscription, reset inactive memberships to active link to the active subscription. + * + * @param string $post_status Post status. + * @param WP_Post $post Post object. + * + * @return string + */ + public static function check_membership_status( $post_status, $post ) { + if ( 'wc_user_membership' !== $post->post_type || 'wcm-active' === $post->post_status || ! self::is_active() || ! function_exists( 'wc_memberships_get_user_membership' ) ) { + return $post_status; + } + $integrations = wc_memberships()->get_integrations_instance(); + $integration = $integrations ? $integrations->get_subscriptions_instance() : null; + $membership = wc_memberships_get_user_membership( $post->ID ); + $plan_id = $membership->get_plan_id(); + if ( $integration && $integration->has_membership_plan_subscription( $plan_id ) ) { + $subscription_plan = new \WC_Memberships_Integration_Subscriptions_Membership_Plan( $plan_id ); + $required_products = $subscription_plan->get_subscription_product_ids(); + $active_subscriptions = WooCommerce_Connection::get_active_subscriptions_for_user( $membership->get_user_id(), $required_products ); + $has_subscription = ! empty( $active_subscriptions ); + if ( $has_subscription ) { + $post_status = 'wcm-active'; + $membership = new \WC_Memberships_Integration_Subscriptions_User_Membership( $post->ID ); + $membership->unschedule_expiration_events(); + $membership->set_subscription_id( $active_subscriptions[0] ); + $membership->set_end_date(); // Clear the end date. + $membership->update_status( 'active' ); + } + } + + return $post_status; + } + /** * Deactivate the cron job. */ @@ -952,6 +1009,7 @@ public static function fix_expired_memberships_for_active_subscriptions() { foreach ( $memberships as $membership ) { // If the membership is not active and has an end date in the past, reactivate it. if ( $membership && ! $membership->has_status( $active_membership_statuses ) && $membership->has_end_date() && $membership->get_end_date( 'timestamp' ) < time() ) { + $membership->unschedule_expiration_events(); $membership->set_end_date(); // Clear the end date. $membership->update_status( 'active' ); // Reactivate the membership. $reactivated_memberships++; diff --git a/includes/reader-revenue/woocommerce/class-woocommerce-connection.php b/includes/reader-revenue/woocommerce/class-woocommerce-connection.php index afc7ee9df0..3f99a13cb6 100644 --- a/includes/reader-revenue/woocommerce/class-woocommerce-connection.php +++ b/includes/reader-revenue/woocommerce/class-woocommerce-connection.php @@ -148,6 +148,41 @@ public static function get_batch_of_active_subscriptions( $batch_size = 100, $of return ! empty( $subscriptions ) ? array_values( $subscriptions ) : false; } + /** + * Does the given user have any subscriptions with an active status? + * Can optionally pass an array of product IDs. If given, only subscriptions + * that have at least one of the given product IDs will be returned. + * + * @param int $user_id User ID. + * @param array $product_ids Optional array of product IDs to filter by. + * + * @return int[] Array of active subscription IDs. + */ + public static function get_active_subscriptions_for_user( $user_id, $product_ids = [] ) { + $subcriptions = array_reduce( + array_keys( \wcs_get_users_subscriptions( $user_id ) ), + function( $acc, $subscription_id ) use ( $product_ids ) { + $subscription = \wcs_get_subscription( $subscription_id ); + if ( $subscription->has_status( self::ACTIVE_SUBSCRIPTION_STATUSES ) ) { + if ( ! empty( $product_ids ) ) { + foreach ( $product_ids as $product_id ) { + if ( $subscription->has_product( $product_id ) ) { + $acc[] = $subscription_id; + return $acc; + } + } + } else { + $acc[] = $subscription_id; + } + } + return $acc; + }, + [] + ); + + return $subcriptions; + } + /** * Get the most recent active subscription, or the last successful order for a given customer. * @@ -160,20 +195,18 @@ public static function get_last_successful_order( $customer ) { return false; } + $user_id = $customer->get_id(); + // Prioritize any currently active subscriptions. - $user_subscriptions = \wcs_get_users_subscriptions( $customer->get_id() ); - if ( ! empty( $user_subscriptions ) ) { - foreach ( $user_subscriptions as $subscription ) { - if ( $subscription->has_status( self::ACTIVE_SUBSCRIPTION_STATUSES ) ) { - return $subscription; - } - } + $active_subscriptions = self::get_active_subscriptions_for_user( $user_id ); + if ( ! empty( $active_subscriptions ) ) { + return reset( $active_subscriptions ); } // If no active subscriptions, get the most recent completed order. // See https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query for query args. $args = [ - 'customer_id' => $customer->get_id(), + 'customer_id' => $user_id, 'status' => [ 'wc-completed' ], 'limit' => 1, 'order' => 'DESC',