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
4 changes: 4 additions & 0 deletions includes/class-rest-authenticaton.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ class Rest_Authenticaton {
'endpoint' => '|^/wc/v2/memberships/plans|',
'callback' => [ __CLASS__, 'add_filter_for_woo_read_endpoints' ],
],
'get-woo-memberships' => [
'endpoint' => '|^/wc/v2/memberships|',
'callback' => [ __CLASS__, 'add_filter_for_woo_read_endpoints' ],
],
];

/**
Expand Down
2 changes: 2 additions & 0 deletions includes/cli/backfillers/class-reader-registered.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ public function get_events() {
]
);

WP_CLI::line( '' );
WP_CLI::line( sprintf( 'Found %s user(s) eligible for sync.', count( $users ) ) );
WP_CLI::line( '' );

$this->maybe_initialize_progress_bar( 'Processing users', count( $users ) );

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ public function get_events() {
$this->maybe_initialize_progress_bar( 'Processing memberships', count( $membership_posts_ids ) );

$events = [];
WP_CLI::line( '' );
WP_CLI::line( sprintf( 'Found %s membership(s) eligible for sync.', count( $membership_posts_ids ) ) );
WP_CLI::line( '' );

foreach ( $membership_posts_ids as $post_id ) {
$membership = new \WC_Memberships_User_Membership( $post_id );
Expand All @@ -77,6 +80,7 @@ public function get_events() {
'membership_id' => $membership->get_id(),
'new_status' => $status,
];
$timestamp = null;
switch ( $status ) {
case 'paused':
$timestamp = strtotime( $membership->get_paused_date() );
Expand Down
70 changes: 58 additions & 12 deletions includes/hub/admin/class-membership-plans-table.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,25 +22,54 @@ class Membership_Plans_Table extends \WP_List_Table {
*/
public function get_columns() {
$columns = [
'id' => __( 'ID', 'newspack-network' ),
'name' => __( 'Name', 'newspack-network' ),
];
$columns['site_url'] = __( 'Site URL', 'newspack-network' );
$columns['network_pass_id'] = __( 'Network ID', 'newspack-network' );
if ( \Newspack_Network\Admin::use_experimental_auditing_features() ) {
$columns['active_members_count'] = __( 'Active Members', 'newspack-network' );
$columns['network_pass_discrepancies'] = __( 'Discrepancies', 'newspack-network' );
$columns['active_memberships_count'] = __( 'Active Memberships', 'newspack-network' );
$columns['network_pass_discrepancies'] = __( 'Membership Discrepancies', 'newspack-network' );

$active_subscriptions_sum = array_reduce(
$this->items,
function( $carry, $item ) {
return $carry + ( is_numeric( $item['active_subscriptions_count'] ) ? $item['active_subscriptions_count'] : 0 );
},
0
);
$subs_info = sprintf(
' <span class="dashicons dashicons-info-outline" title="%s"></span>',
__( 'Active Subscriptions tied to this membership plan', 'newspack-network' )
);
// translators: %d is the sum of active subscriptions.
$columns['active_subscriptions_count'] = sprintf( __( 'Active Subscriptions (%d)', 'newspack-network' ), $active_subscriptions_sum ) . $subs_info;
}
$columns['links'] = __( 'Links', 'newspack-network' );
return $columns;
}

/**
* Prepare items to be displayed
*/
public function prepare_items() {
$this->_column_headers = [ $this->get_columns(), [], [], 'id' ];
$this->items = Membership_Plans::get_membershp_plans_from_network();
$membership_plans_from_network_data = Membership_Plans::get_membership_plans_from_network();

// Handle table sorting.
$order = isset( $_REQUEST['order'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
$orderby = isset( $_REQUEST['orderby'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['orderby'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended
if ( $order && $orderby ) {
usort(
$membership_plans_from_network_data['plans'],
function( $a, $b ) use ( $orderby, $order ) {
if ( $order === 'asc' ) {
return $a[ $orderby ] <=> $b[ $orderby ];
}
return $b[ $orderby ] <=> $a[ $orderby ];
}
);
}

$this->items = $membership_plans_from_network_data['plans'];
$this->_column_headers = [ $this->get_columns(), [], $this->get_sortable_columns(), 'id' ];
}

/**
Expand All @@ -53,6 +82,10 @@ public function prepare_items() {
public function column_default( $item, $column_name ) {
$memberships_list_url = sprintf( '%s/wp-admin/edit.php?s&post_status=wcm-active&post_type=wc_user_membership&post_parent=%d', $item['site_url'], $item['id'] );

if ( $column_name === 'name' ) {
$edit_url = sprintf( '%s/wp-admin/post.php?post=%d&action=edit', $item['site_url'], $item['id'] );
return sprintf( '<a href="%s">%s</a>', esc_url( $edit_url ), $item[ $column_name ] . ' (#' . $item['id'] . ')' );
}
if ( $column_name === 'network_pass_id' && $item[ $column_name ] ) {
return sprintf( '<code>%s</code>', $item[ $column_name ] );
}
Expand All @@ -65,7 +98,15 @@ public function column_default( $item, $column_name ) {

$memberships_list_url_with_emails_url = add_query_arg(
\Newspack_Network\Woocommerce_Memberships\Admin::MEMBERSHIPS_TABLE_EMAILS_QUERY_PARAM,
implode( ',', $discrepancies ),
implode(
',',
array_map(
function( $email_address ) {
return urlencode( $email_address );
},
$discrepancies
)
),
$memberships_list_url
);
$message = sprintf(
Expand All @@ -80,13 +121,18 @@ public function column_default( $item, $column_name ) {
);
return sprintf( '<a href="%s">%s</a>', esc_url( $memberships_list_url_with_emails_url ), esc_html( $message ) );
}
if ( $column_name === 'links' ) {
$edit_url = sprintf( '%s/wp-admin/post.php?post=%d&action=edit', $item['site_url'], $item['id'] );
return sprintf( '<a href="%s">%s</a>', esc_url( $edit_url ), esc_html__( 'Edit', 'newspack-network' ) );
}
if ( $column_name === 'active_members_count' && $item[ $column_name ] ) {
if ( $column_name === 'active_memberships_count' && isset( $item[ $column_name ] ) ) {
return sprintf( '<a href="%s">%s</a>', esc_url( $memberships_list_url ), $item[ $column_name ] );
}
return isset( $item[ $column_name ] ) ? $item[ $column_name ] : '';
}

/**
* Get sortable columns.
*/
public function get_sortable_columns() {
return [
'network_pass_id' => [ 'network_pass_id', false, __( 'Network Pass ID' ), __( 'Table ordered by Network Pass ID.' ) ],
];
}
}
86 changes: 54 additions & 32 deletions includes/hub/admin/class-membership-plans.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public static function render() {
<?php
printf(
/* translators: last fetch date. */
esc_html__( 'Plans from Nodes were last fetched on %s.', 'newspack-network' ),
esc_html__( 'Plans were last fetched on %s.', 'newspack-network' ),
esc_html( gmdate( 'Y-m-d H:i', (int) $plans_cache['last_updated'] ) )
);
?>
Expand All @@ -75,21 +75,20 @@ public static function render() {
* @param \Newspack_Network\Node\Node $node The node.
* @param string $collection_endpoint The collection endpoint.
* @param string $collection_endpoint_id The collection endpoint ID.
* @param array $query_args The query args.
*/
public static function fetch_collection_from_api( $node, $collection_endpoint, $collection_endpoint_id ) {
$endpoint = sprintf( '%s/wp-json/%s', $node->get_url(), $collection_endpoint );
if ( Network_Admin::use_experimental_auditing_features() ) {
$endpoint = add_query_arg( 'include_active_members_emails', 1, $endpoint );
}
public static function fetch_collection_from_api( $node, $collection_endpoint, $collection_endpoint_id, $query_args = [] ) {
$endpoint = add_query_arg( $query_args, sprintf( '%s/wp-json/%s', $node->get_url(), $collection_endpoint ) );
$response = wp_remote_get( // phpcs:ignore
$endpoint,
[
'headers' => $node->get_authorization_headers( 'get-woo-' . $collection_endpoint_id ),
'timeout' => 60, // phpcs:ignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nonblocking nit: Does the default not work here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean? Default timeout is 5.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. Whoops. For some reason I thought this was 30. Nevermind!

]
);
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
Debugger::log( 'API request for node\'s memberships failed' );
return;
return null;
}
return json_decode( wp_remote_retrieve_body( $response ) );
}
Expand All @@ -104,27 +103,32 @@ private static function get_membership_plans_from_cache() {
/**
* Get membership plans from all nodes.
*/
public static function get_membershp_plans_from_network() {
public static function get_membership_plans_from_network() {
$plans_cache = self::get_membership_plans_from_cache();
if ( $plans_cache && isset( $plans_cache['plans'] ) ) {
return $plans_cache['plans'];
return $plans_cache;
}
$by_network_pass_id = [];
$membership_plans = [];

if ( Network_Admin::use_experimental_auditing_features() ) {
$local_membership_plans = self::get_local_membership_plans();
foreach ( $local_membership_plans as $local_plan ) {
if ( $local_plan['network_pass_id'] ) {
$by_network_pass_id[ $local_plan['network_pass_id'] ][ $local_plan['site_url'] ] = $local_plan['active_members_emails'];
}
$local_membership_plans = self::get_local_membership_plans();
foreach ( $local_membership_plans as $local_plan ) {
if ( $local_plan['network_pass_id'] ) {
$by_network_pass_id[ $local_plan['network_pass_id'] ][ $local_plan['site_url'] ] = $local_plan['active_members_emails'];
}
$membership_plans = array_merge( $local_membership_plans, $membership_plans );
}
$membership_plans = array_merge( $local_membership_plans, $membership_plans );

$nodes = \Newspack_Network\Hub\Nodes::get_all_nodes();
foreach ( $nodes as $node ) {
$node_plans = self::fetch_collection_from_api( $node, 'wc/v2/memberships/plans', 'membership-plans' );
$query_args = [];
if ( \Newspack_Network\Admin::use_experimental_auditing_features() ) {
$query_args['include_active_members_emails'] = 1;
}
$node_plans = self::fetch_collection_from_api( $node, 'wc/v2/memberships/plans', 'membership-plans', $query_args );
if ( $node_plans === null ) {
continue;
}
foreach ( $node_plans as $plan ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting the following warning on this line:

PHP Warning:  foreach() argument must be of type array|object, null given in /Users/raz/Sites/newspack/network/includes/hub/admin/class-membership-plans.php on line 129

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a4ab6df

$network_pass_id = null;
foreach ( $plan->meta_data as $meta ) {
Expand All @@ -139,15 +143,17 @@ public static function get_membershp_plans_from_network() {
$by_network_pass_id[ $network_pass_id ][ $node->get_url() ] = $plan->active_members_emails;
}
$membership_plans[] = [
'id' => $plan->id,
'site_url' => $node->get_url(),
'name' => $plan->name,
'network_pass_id' => $network_pass_id,
'active_members_count' => $plan->active_members_count,
'id' => $plan->id,
'site_url' => $node->get_url(),
'name' => $plan->name,
'network_pass_id' => $network_pass_id,
'active_memberships_count' => $plan->active_memberships_count,
'active_subscriptions_count' => $plan->active_subscriptions_count,
];
}
}

$discrepancies_emails = [];
if ( Network_Admin::use_experimental_auditing_features() ) {
$discrepancies = [];
foreach ( $by_network_pass_id as $plan_network_pass_id => $by_site ) {
Expand All @@ -156,6 +162,15 @@ public static function get_membershp_plans_from_network() {
$discrepancies[ $plan_network_pass_id ][ $site_url ] = array_diff( $emails, $shared_emails );
}
}

// Get all emails which are discrepant across all sites.
foreach ( $discrepancies as $plan_network_id => $plan_discrepancies ) {
foreach ( $plan_discrepancies as $site_url => $plan_site_discrepancies ) {
$discrepancies_emails = array_merge( $discrepancies_emails, $plan_site_discrepancies );
}
}
$discrepancies_emails = array_unique( $discrepancies_emails );

$membership_plans = array_map(
function( $plan ) use ( $discrepancies ) {
if ( isset(
Expand All @@ -170,12 +185,13 @@ function( $plan ) use ( $discrepancies ) {
$membership_plans
);
}
$plans_to_save = [
'plans' => $membership_plans,
'last_updated' => time(),
$memberships_data = [
'plans' => $membership_plans,
'discrepancies_emails' => $discrepancies_emails,
'last_updated' => time(),
];
update_option( self::OPTIONS_CACHE_KEY_PLANS, $plans_to_save );
return $membership_plans;
update_option( self::OPTIONS_CACHE_KEY_PLANS, $memberships_data );
return $memberships_data;
}

/**
Expand All @@ -187,15 +203,21 @@ public static function get_local_membership_plans() {
return [];
}
foreach ( wc_memberships_get_membership_plans() as $plan ) {
$network_pass_id = get_post_meta( $plan->post->ID, \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, true );
$plan_data = [
'id' => $plan->post->ID,
'site_url' => get_site_url(),
'name' => $plan->post->post_title,
'network_pass_id' => get_post_meta( $plan->post->ID, \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, true ),
'active_members_count' => $plan->get_memberships_count( 'active' ),
'id' => $plan->post->ID,
'site_url' => get_site_url(),
'name' => $plan->post->post_title,
'network_pass_id' => $network_pass_id,
'active_memberships_count' => $plan->get_memberships_count( 'active' ),
];
if ( Network_Admin::use_experimental_auditing_features() ) {
$plan_data['active_members_emails'] = \Newspack_Network\Woocommerce_Memberships\Admin::get_active_members_emails( $plan );
if ( $network_pass_id ) {
$plan_data['active_subscriptions_count'] = \Newspack_Network\Woocommerce_Memberships\Admin::get_plan_related_active_subscriptions( $plan );
} else {
$plan_data['active_subscriptions_count'] = __( 'Only displayed for plans with a Network ID.', 'newspack-network' );
}
}
$membership_plans[] = $plan_data;
}
Expand Down
40 changes: 37 additions & 3 deletions includes/woocommerce-memberships/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ public static function init() {
add_filter( 'post_row_actions', array( __CLASS__, 'post_row_actions' ), 99, 2 ); // After the Memberships plugin.
add_filter( 'map_meta_cap', array( __CLASS__, 'map_meta_cap' ), 20, 4 );
add_filter( 'wc_memberships_rest_api_membership_plan_data', [ __CLASS__, 'add_data_to_membership_plan_response' ], 2, 3 );
add_filter( 'woocommerce_rest_prepare_wc_user_membership', [ __CLASS__, 'add_data_to_wc_user_membership_response' ], 2, 3 );
add_filter( 'request', [ __CLASS__, 'request_query' ] );
add_action( 'pre_user_query', [ __CLASS__, 'pre_user_query' ] );
add_action( 'admin_notices', [ __CLASS__, 'admin_notices' ] );
Expand Down Expand Up @@ -100,14 +101,47 @@ function ( $membership ) {
*/
public static function add_data_to_membership_plan_response( $data, $plan, $request ) {
if ( $request && isset( $request->get_headers()['x_np_network_signature'] ) ) {
$data['active_members_count'] = $plan->get_memberships_count( 'active' );
if ( $request->get_param( 'include_active_members_emails' ) ) {
$data['active_members_emails'] = self::get_active_members_emails( $plan );
$data['active_memberships_count'] = $plan->get_memberships_count( 'active' );
$network_pass_id = get_post_meta( $plan->id, self::NETWORK_ID_META_KEY, true );
if ( $network_pass_id && $request->get_param( 'include_active_members_emails' ) ) {
$data['active_subscriptions_count'] = self::get_plan_related_active_subscriptions( $plan );
$data['active_members_emails'] = array_values( array_unique( self::get_active_members_emails( $plan ) ) );
} else {
$data['active_subscriptions_count'] = __( 'Only displayed for plans with a Network ID.', 'newspack-network' );
}
}
return $data;
}

/**
* Get the active subscriptions related to a membership plan.
*
* @param \WC_Memberships_Membership_Plan $plan The membership plan.
*/
public static function get_plan_related_active_subscriptions( $plan ) {
$product_ids = $plan->get_product_ids();
$subscriptions = wcs_get_subscriptions_for_product( $product_ids, 'ids', [ 'subscription_status' => 'active' ] );
return count( $subscriptions );
}

/**
* Filter user membership data from REST API.
*
* @param \WP_REST_Response $response the response object.
* @param null|\WP_Post $user the user membership post object.
* @param \WP_REST_Request $request the request object.
*/
public static function add_data_to_wc_user_membership_response( $response, $user, $request ) {
if ( $request && isset( $request->get_headers()['x_np_network_signature'] ) ) {
// Add network plan ID to the response.
$plan = wc_memberships_get_membership_plan( $response->data['plan_id'] );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like wc_memberships_get_membership_plan can return false, so we will probably need to check the value of $plan before adding to response data.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in a4ab6df

if ( $plan !== false ) {
$response->data['plan_network_id'] = get_post_meta( $plan->id, self::NETWORK_ID_META_KEY, true );
}
}
return $response;
}

/**
* Adds a meta box to the membership plan edit screen.
*/
Expand Down