Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions includes/class-initializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -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\Database\Subscriptions::init();
Hub\Database\Orders::init();
Expand Down Expand Up @@ -59,6 +60,7 @@ public static function init() {
Woocommerce_Memberships\Events::init();

Woocommerce\Events::init();
Woocommerce_Memberships\Subscriptions_Integration::init();

register_activation_hook( NEWSPACK_NETWORK_PLUGIN_FILE, [ __CLASS__, 'activation_hook' ] );
}
Expand Down
94 changes: 94 additions & 0 deletions includes/hub/class-network-data-endpoint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* Newspack Network_Data Endpoint.
*
* @package Newspack
*/

namespace Newspack_Network\Hub;

use Newspack_Network\Debugger;
use WP_REST_Response;
use WP_REST_Request;
use WP_REST_Server;

/**
* Class to handle the Endpoint that Nodes will reach to pull new data from
*/
class Network_Data_Endpoint {
/**
* Runs the initialization.
*
* @return void
*/
public static function init() {
add_action( 'rest_api_init', [ __CLASS__, 'register_routes' ] );
}

/**
* Register the routes for the objects of the controller.
*/
public static function register_routes() {
register_rest_route(
'newspack-network/v1',
'/network-subscriptions',
[
[
'methods' => \WP_REST_Server::READABLE,
'callback' => [ __CLASS__, 'api_get_network_subscriptions' ],
'permission_callback' => '__return_true', // Auth will be handled by \Newspack_Network\Utils\Requests::get_request_to_hub_errors.
],
]
);
}

/**
* Get active subscription IDs from the network.
*
* @param string $email Email of the user.
* @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_ids, $site = false ) {
$active_subscriptions_ids = [];
foreach ( Nodes::get_all_nodes() as $node ) {
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,
\Newspack_Network\Utils\Users::get_users_active_subscriptions_tied_to_network_ids( $email, $plan_network_ids )
);
}
return $active_subscriptions_ids;
}

/**
* Handle the request for active subscriptions tied to a network plan.
*
* @param WP_REST_Request $request Full data about the request.
* @return WP_REST_Response
*/
public static function api_get_network_subscriptions( $request ) {
$request_error = \Newspack_Network\Utils\Requests::get_request_to_hub_errors( $request );
if ( \is_wp_error( $request_error ) ) {
return new WP_REST_Response( [ 'error' => $request_error->get_error_message() ], 403 );
}
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_ids'], $request['site'] ),
]
);
}
}
26 changes: 25 additions & 1 deletion includes/hub/class-node.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand Down Expand Up @@ -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 ) );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );

Expand Down Expand Up @@ -108,7 +112,7 @@ public function update_membership() {
)
);

Debugger::log( 'User membership updated' );
Debugger::log( 'User membership updated.' );
}

/**
Expand Down
38 changes: 35 additions & 3 deletions includes/node/class-info-endpoints.php
Original file line number Diff line number Diff line change
Expand Up @@ -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() );
}

/**
Expand All @@ -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'] )
);
}
}
49 changes: 49 additions & 0 deletions includes/utils/class-users.php
Original file line number Diff line number Diff line change
Expand Up @@ -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::MEMBERSHIP_PLANS_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;
}
}
6 changes: 2 additions & 4 deletions includes/woocommerce-memberships/class-admin.php
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
<?php
/**
* Newspack Network Admin customizations for woocommerce memberships.
* Newspack Network Admin customizations for WooCommerce Memberships.
*
* @package Newspack
*/

namespace Newspack_Network\Woocommerce_Memberships;

/**
* Handles admin tweaks for woocommerce memberships.
*
* Adds a metabox to the membership plan edit screen to allow the user to add a network id metadata to the plans
* Handles admin tweaks for WooCommerce Memberships.
*/
class Admin {

Expand Down
2 changes: 1 addition & 1 deletion includes/woocommerce-memberships/class-events.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php
/**
* Newspack Network Woo Membership events
* Newspack Network WooCommerce Memberships events
*
* @package Newspack
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php
/**
* Newspack Network WooCommerce Subscriptions integration for WooCommerce Memberships.
*
* @package Newspack
*/

namespace Newspack_Network\Woocommerce_Memberships;

/**
* Handles tweaks for WooCommerce Memberships WooCommerce Subscriptions integration.
*/
class Subscriptions_Integration {
/**
* Runs the initialization.
*
* @return void
*/
public static function init() {
// You'd think this first filter is enough, but it's not. Even if the membership cancellation via linked subscription
// is prevented, the expiry code is also executed.
add_filter( 'wc_memberships_cancel_subscription_linked_membership', [ __CLASS__, 'prevent_membership_expiration' ], 10, 2 );
add_filter( 'wc_memberships_expire_user_membership', [ __CLASS__, 'prevent_membership_expiration' ], 10, 2 );
}

/**
* Prevent membership expiration if another network site has a synced membership active.
*
* @param bool $cancel_or_expire whether to cancel/expire the membership when the subscription is cancelled (default true).
* @param \WC_Memberships_Integration_Subscriptions_User_Membership $user_membership the subscription-tied membership.
*/
public static function prevent_membership_expiration( $cancel_or_expire, $user_membership ) {
$user_email = get_userdata( $user_membership->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;
}
}