Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 = $_REQUEST['order'] ?? false; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
$orderby = $_REQUEST['orderby'] ?? false; // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we sanitize these to address the WordPress.Security.ValidatedSanitizedInput.InputNotSanitized warning?

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in a4ab6df

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.' ) ],
];
}
}
81 changes: 50 additions & 31 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,16 +75,15 @@ 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 ) ) {
Expand All @@ -104,27 +103,29 @@ 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 );
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 +140,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 +159,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 +182,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 +200,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
38 changes: 35 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,45 @@ 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

$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