diff --git a/includes/class-initializer.php b/includes/class-initializer.php index 2734ee34..e887f681 100644 --- a/includes/class-initializer.php +++ b/includes/class-initializer.php @@ -24,6 +24,7 @@ public static function init() { Hub\Nodes::init(); Hub\Webhook::init(); Hub\Pull_Endpoint::init(); + Hub\Network_Data_Endpoint::init(); Hub\Event_Listeners::init(); Hub\Newspack_Ads_GAM::init(); Hub\Connect_Node::init(); @@ -55,6 +56,7 @@ public static function init() { Woocommerce_Memberships\Admin::init(); Woocommerce_Memberships\Events::init(); + Woocommerce_Memberships\Subscriptions_Integration::init(); Woocommerce_Subscriptions\Admin::init(); register_activation_hook( NEWSPACK_NETWORK_PLUGIN_FILE, [ __CLASS__, 'activation_hook' ] ); diff --git a/includes/hub/class-network-data-endpoint.php b/includes/hub/class-network-data-endpoint.php index b9a3d905..ab3e4697 100644 --- a/includes/hub/class-network-data-endpoint.php +++ b/includes/hub/class-network-data-endpoint.php @@ -36,7 +36,7 @@ public static function register_routes() { [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ __CLASS__, 'api_get_network_subscriptions' ], - 'permission_callback' => '__return_true', + 'permission_callback' => '__return_true', // Auth will be handled by \Newspack_Network\Utils\Requests::get_request_to_hub_errors. ], ] ); @@ -46,26 +46,28 @@ public static function register_routes() { * Get active subscription IDs from the network. * * @param string $email Email of the user. - * @param string $plan_network_id Network ID of the plan. + * @param string $plan_network_ids Network ID of the plan. * @param string $site Site URL. */ - public static function get_active_subscription_ids_from_network( $email, $plan_network_id, $site ) { + public static function get_active_subscription_ids_from_network( $email, $plan_network_ids, $site = false ) { $active_subscriptions_ids = []; foreach ( Nodes::get_all_nodes() as $node ) { - if ( $site === $node->get_url() ) { - // Skip the node which is making the request. It's only interested in the other nodes. + if ( $site && $site === $node->get_url() ) { + // Skip the node which is making the request. continue; } + $node_subscription_ids = $node->get_subscriptions_with_network_plans( $email, $plan_network_ids ); + if ( is_array( $node_subscription_ids ) ) { + $active_subscriptions_ids = array_merge( $active_subscriptions_ids, $node_subscription_ids ); + } + } + // Also look on the Hub itself, unless the $site was provided. + if ( $site !== false ) { $active_subscriptions_ids = array_merge( $active_subscriptions_ids, - $node->get_subscriptions_with_network_plan( $email, $plan_network_id ) + \Newspack_Network\Utils\Users::get_users_active_subscriptions_tied_to_network_ids( $email, $plan_network_ids ) ); } - // Also look on the Hub itself. - $active_subscriptions_ids = array_merge( - $active_subscriptions_ids, - \Newspack_Network\Utils\Users::get_users_active_subscriptions_tied_to_network_id( $email, $plan_network_id ) - ); return $active_subscriptions_ids; } @@ -80,13 +82,12 @@ public static function api_get_network_subscriptions( $request ) { if ( \is_wp_error( $request_error ) ) { return new WP_REST_Response( [ 'error' => $request_error->get_error_message() ], 403 ); } - if ( ! isset( $request['plan_network_id'] ) || empty( $request['plan_network_id'] ) ) { - return new WP_REST_Response( [ 'error' => __( 'Missing plan_network_id', 'newspack-network' ) ], 400 ); + if ( ! isset( $request['plan_network_ids'] ) || empty( $request['plan_network_ids'] ) ) { + return new WP_REST_Response( [ 'error' => __( 'Missing plan_network_ids', 'newspack-network' ) ], 400 ); } - return new WP_REST_Response( [ - 'active_subscriptions_ids' => self::get_active_subscription_ids_from_network( $request['email'], $request['plan_network_id'], $request['site'] ), + 'active_subscriptions_ids' => self::get_active_subscription_ids_from_network( $request['email'], $request['plan_network_ids'], $request['site'] ), ] ); } diff --git a/includes/hub/class-node.php b/includes/hub/class-node.php index db90885b..e44aeda3 100644 --- a/includes/hub/class-node.php +++ b/includes/hub/class-node.php @@ -12,7 +12,7 @@ use WP_Post; /** - * Class to represent one Node of the netowrk + * Class to represent a Node in the network */ class Node { /** @@ -179,6 +179,30 @@ public function get_site_info() { $this->get_url() . '/wp-json/newspack-network/v1/info', [ 'headers' => $this->get_authorization_headers( 'info' ), + 'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout + ] + ); + return json_decode( wp_remote_retrieve_body( $response ) ); + } + + /** + * Get all subscriptions of a user, related to provided network plan IDs. + * + * @param string $email The email to get subscriptions for. + * @param string $plan_network_ids The plan network ID to get subscriptions for. + */ + public function get_subscriptions_with_network_plans( $email, $plan_network_ids ) { + $response = wp_remote_get( // phpcs:ignore + add_query_arg( + [ + 'email' => $email, + 'plan_network_ids' => $plan_network_ids, + ], + $this->get_url() . '/wp-json/newspack-network/v1/subscriptions' + ), + [ + 'headers' => $this->get_authorization_headers( 'subscriptions' ), + 'timeout' => 60, // phpcs:ignore WordPressVIPMinimum.Performance.RemoteRequestTimeout.timeout_timeout ] ); return json_decode( wp_remote_retrieve_body( $response ) ); diff --git a/includes/incoming-events/class-woocommerce-membership-updated.php b/includes/incoming-events/class-woocommerce-membership-updated.php index 1e4c1e5d..d4eeedd4 100644 --- a/includes/incoming-events/class-woocommerce-membership-updated.php +++ b/includes/incoming-events/class-woocommerce-membership-updated.php @@ -73,6 +73,10 @@ public function update_membership() { User_Update_Watcher::$enabled = false; $user = User_Utils::get_or_create_user_by_email( $email, $this->get_site(), $this->data->user_id ?? '' ); + if ( ! $user ) { + Debugger::log( 'User not found.' ); + return; + } $user_membership = wc_memberships_get_user_membership( $user->ID, $local_plan_id ); @@ -108,7 +112,7 @@ public function update_membership() { ) ); - Debugger::log( 'User membership updated' ); + Debugger::log( 'User membership updated.' ); } /** diff --git a/includes/node/class-info-endpoints.php b/includes/node/class-info-endpoints.php index db57cff4..87fae680 100644 --- a/includes/node/class-info-endpoints.php +++ b/includes/node/class-info-endpoints.php @@ -31,12 +31,32 @@ public static function register_routes() { [ 'methods' => \WP_REST_Server::READABLE, 'callback' => [ __CLASS__, 'handle_info_request' ], - 'permission_callback' => function( $request ) { - return \Newspack_Network\Rest_Authenticaton::verify_signature( $request, 'info', Settings::get_secret_key() ); - }, + 'permission_callback' => [ __CLASS__, 'permission_callback' ], ], ] ); + register_rest_route( + 'newspack-network/v1', + '/subscriptions', + [ + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ __CLASS__, 'handle_subscriptions_request' ], + 'permission_callback' => [ __CLASS__, 'permission_callback' ], + ], + ] + ); + } + + /** + * The permission callback. + * + * @param WP_REST_Request $request Full data about the request. + */ + public static function permission_callback( $request ) { + $route = $request->get_route(); + $request_slug = substr( $route, strrpos( $route, '/' ) + 1 ); + return \Newspack_Network\Rest_Authenticaton::verify_signature( $request, $request_slug, Settings::get_secret_key() ); } /** @@ -53,4 +73,16 @@ public static function handle_info_request() { ] ); } + + /** + * Handles the subscriptions request. + * Will return the active subscription IDs for the given email, when matching a membership by plan network ID. + * + * @param WP_REST_Request $request Full data about the request. + */ + public static function handle_subscriptions_request( $request ) { + return rest_ensure_response( + \Newspack_Network\Utils\Users::get_users_active_subscriptions_tied_to_network_ids( $request['email'], $request['plan_network_ids'] ) + ); + } } diff --git a/includes/utils/class-users.php b/includes/utils/class-users.php index c4baeaa1..9ec18dbb 100644 --- a/includes/utils/class-users.php +++ b/includes/utils/class-users.php @@ -238,4 +238,53 @@ public static function get_not_synchronized_users_emails() { public static function get_not_synchronized_users_count() { return count( self::get_not_synchronized_users( [ 'id' ] ) ); } + + /** + * Get user's active subscriptions tied to a network ID. + * + * @param string $email The email of the user to look for. + * @param array $plan_network_ids Network IDs of the plans. + * @return array Array of active subscription IDs. + */ + public static function get_users_active_subscriptions_tied_to_network_ids( $email, $plan_network_ids ) { + if ( ! function_exists( 'wcs_get_subscriptions' ) ) { + return []; + } + $user = get_user_by( 'email', $email ); + if ( ! $user ) { + return []; + } + // If a membership is a "shadowed" membership, no subscription will be tied to it. + // The relevant subscription has to be found by matching the plan-granting products with subscriptions. + $plan_ids = get_posts( + [ + 'meta_key' => \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, + 'meta_value' => $plan_network_ids, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + 'meta_compare' => 'IN', + 'post_type' => \Newspack_Network\Woocommerce_Memberships\Admin::MEMBERSHIPS_CPT, + 'field' => 'ID', + ] + ); + $product_ids = []; + foreach ( $plan_ids as $plan_id ) { + // Get the products that grant membership in the plan. + $product_ids = array_merge( $product_ids, get_post_meta( $plan_id->ID, '_product_ids', true ) ); + } + // Get any active subscriptions for these product IDs. + $active_subscription_ids = []; + foreach ( $product_ids as $product_id ) { + $args = [ + 'customer_id' => $user->ID, + 'product_id' => $product_id, + 'subscription_status' => 'active', + 'subscriptions_per_page' => 1, + ]; + $subscriptions = \wcs_get_subscriptions( $args ); + $subscription = reset( $subscriptions ); + if ( $subscription ) { + $active_subscription_ids[] = $subscription->get_id(); + } + } + return $active_subscription_ids; + } } diff --git a/includes/woocommerce-memberships/class-admin.php b/includes/woocommerce-memberships/class-admin.php index 4ea5cf16..b9160feb 100644 --- a/includes/woocommerce-memberships/class-admin.php +++ b/includes/woocommerce-memberships/class-admin.php @@ -1,6 +1,6 @@ user_id )->user_email; + $membership_plan_id = get_post_meta( $user_membership->get_plan()->get_id(), Admin::NETWORK_ID_META_KEY, true ); + + if ( \Newspack_Network\Site_Role::is_hub() ) { + $active_subscriptions_ids = \Newspack_Network\Hub\Network_Data_Endpoint::get_active_subscription_ids_from_network( + $user_email, + [ $membership_plan_id ] + ); + } else { + $params = [ + 'site' => get_bloginfo( 'url' ), + 'plan_network_ids' => [ $membership_plan_id ], + 'email' => $user_email, + ]; + $response = \Newspack_Network\Utils\Requests::request_to_hub( 'wp-json/newspack-network/v1/network-subscriptions', $params, 'GET' ); + if ( is_wp_error( $response ) ) { + return $cancel_or_expire; + } + $active_subscriptions_ids = json_decode( wp_remote_retrieve_body( $response ) )->active_subscriptions_ids ?? []; + } + $can_cancel = empty( $active_subscriptions_ids ); + if ( ! $can_cancel ) { + $user_membership->add_note( + __( 'Membership is not cancelled, because there is at least one active subscription linked to the membership plan on the network.', 'newspack-plugin' ) + ); + } + return $can_cancel ? $cancel_or_expire : false; + } +}