From 0c31fd60eeb404ab36f0d5eff7482f4cd4a5085e Mon Sep 17 00:00:00 2001 From: Adam Cassis Date: Wed, 10 Apr 2024 10:03:39 +0200 Subject: [PATCH] feat: experimental auditing features (#79) * feat(nodes): display sync users count in Nodes table * refactor: move user table handling * feat(users-table): add original user link * feat(membership-plans): display active memberships count * feat(memberships): single table; experimental discrepancies column * feat(memberships): active members link; restrict to specific user * feat: view the discrepant members * feat(subscriptions): sortable columns --- includes/class-admin.php | 14 ++ includes/class-initializer.php | 2 + includes/class-users.php | 109 ++++++++++++++++ .../backfillers/class-reader-registered.php | 10 +- includes/cli/class-data-backfill.php | 3 - .../admin/class-membership-plans-table.php | 67 +++++----- includes/hub/admin/class-membership-plans.php | 77 ++++++++--- includes/hub/admin/class-nodes-list.php | 25 +++- includes/hub/admin/class-subscriptions.php | 65 +++++++++- includes/hub/admin/class-users.php | 78 ------------ includes/hub/admin/class-woo.php | 5 +- includes/hub/admin/css/nodes-list.css | 4 + includes/hub/class-admin.php | 1 - includes/hub/class-node.php | 21 +++ includes/node/class-info-endpoints.php | 53 ++++++++ includes/utils/class-users.php | 24 ++++ .../woocommerce-memberships/class-admin.php | 120 ++++++++++++++++++ 17 files changed, 541 insertions(+), 137 deletions(-) create mode 100644 includes/class-users.php delete mode 100644 includes/hub/admin/class-users.php create mode 100644 includes/node/class-info-endpoints.php diff --git a/includes/class-admin.php b/includes/class-admin.php index 0aa082e9..5ef77d1a 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -191,4 +191,18 @@ public static function admin_init() { */ public static function enqueue_scripts() { } + + /** + * Has experimental auditing features? + * + * @return bool True if experimental auditing features are enabled. + */ + public static function use_experimental_auditing_features() { + $user_slug = defined( 'NEWSPACK_NETWORK_EXPERIMENTAL_AUDITING_USER' ) ? NEWSPACK_NETWORK_EXPERIMENTAL_AUDITING_USER : false; + if ( ! $user_slug ) { + return false; + } + $user = get_user_by( 'login', $user_slug ); + return $user && get_current_user_id() === $user->ID; + } } diff --git a/includes/class-initializer.php b/includes/class-initializer.php index ca7106b6..758e9687 100644 --- a/includes/class-initializer.php +++ b/includes/class-initializer.php @@ -17,6 +17,7 @@ class Initializer { */ public static function init() { Admin::init(); + Users::init(); if ( Site_Role::is_hub() ) { Hub\Admin::init(); @@ -37,6 +38,7 @@ public static function init() { if ( Site_Role::is_node() ) { if ( Node\Settings::get_hub_url() ) { Node\Webhook::init(); + Node\Info_Endpoints::init(); Node\Pulling::init(); Rest_Authenticaton::init_node_filters(); } diff --git a/includes/class-users.php b/includes/class-users.php new file mode 100644 index 00000000..d3d48d3b --- /dev/null +++ b/includes/class-users.php @@ -0,0 +1,109 @@ +%s', + trailingslashit( esc_url( $remote_site ) ), + $remote_id, + sprintf( '%s (#%d)', $remote_site, $remote_id ) + ); + } + } + if ( 'newspack_network_activity' === $column_name && Site_Role::is_hub() ) { + $user = get_user_by( 'id', $user_id ); + if ( ! $user ) { + return $value; + } + + $last_activity = \Newspack_Network\Hub\Stores\Event_Log::get( [ 'email' => $user->user_email ], 1 ); + + if ( empty( $last_activity ) ) { + return '-'; + } + + $last_activity = $last_activity[0]; + + $summary = $last_activity->get_summary(); + $event_log_url = add_query_arg( + [ + 'page' => \Newspack_Network\Hub\Admin\Event_Log::PAGE_SLUG, + 'email' => $user->user_email, + ], + admin_url( 'admin.php' ) + ); + return sprintf( + '%s: %s
%s', + __( 'Last Activity', 'newspack-network' ), + $summary, + $event_log_url, + __( 'View all', 'newspack-network' ) + ); + + } + return $value; + } + + /** + * Handle the filtering of users by multiple roles. + * Unfortunatelly, `get_views` and `get_views_links` are not filterable, so "All" will + * be displayed as the active filter. + * + * @param array $args The current query args. + */ + public static function users_list_table_query_args( $args ) { + if ( isset( $_REQUEST['role__in'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $args['role__in'] = explode( ',', sanitize_text_field( $_REQUEST['role__in'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + unset( $args['role'] ); + } + return $args; + } +} diff --git a/includes/cli/backfillers/class-reader-registered.php b/includes/cli/backfillers/class-reader-registered.php index a2d3385d..7d13a1ea 100644 --- a/includes/cli/backfillers/class-reader-registered.php +++ b/includes/cli/backfillers/class-reader-registered.php @@ -7,6 +7,8 @@ namespace Newspack_Network\Backfillers; +use WP_CLI; + /** * Backfiller class. */ @@ -29,10 +31,14 @@ protected function get_processed_item_output( $event ) { * @return \Newspack_Network\Incoming_Events\Abstract_Incoming_Event[] $events An array of events. */ public function get_events() { + $roles_to_sync = \Newspack_Network\Utils\Users::get_synced_user_roles(); + if ( empty( $roles_to_sync ) ) { + WP_CLI::error( 'Incompatible Newspack plugin version or no roles to sync.' ); + } // Get all users registered between this-> and $end. $users = get_users( [ - 'role__in' => \Newspack\Reader_Activation::get_reader_roles(), + 'role__in' => $roles_to_sync, 'date_query' => [ 'after' => $this->start, 'before' => $this->end, @@ -49,6 +55,8 @@ public function get_events() { ] ); + WP_CLI::line( sprintf( 'Found %s user(s) eligible for sync.', count( $users ) ) ); + $this->maybe_initialize_progress_bar( 'Processing users', count( $users ) ); $events = []; diff --git a/includes/cli/class-data-backfill.php b/includes/cli/class-data-backfill.php index 28107aa6..b20fc9a1 100644 --- a/includes/cli/class-data-backfill.php +++ b/includes/cli/class-data-backfill.php @@ -118,9 +118,6 @@ public static function data_backfill( $args, $assoc_args ) { // phpcs:ignore Gen } WP_CLI::line( '' ); - if ( ! method_exists( '\Newspack\Reader_Activation', 'get_reader_roles' ) ) { - WP_CLI::error( 'Incompatible Newspack plugin version.' ); - } if ( $live ) { WP_CLI::line( '⚡️ Heads up! Running live, data will be updated.' ); } else { diff --git a/includes/hub/admin/class-membership-plans-table.php b/includes/hub/admin/class-membership-plans-table.php index 3653af9e..29770d01 100644 --- a/includes/hub/admin/class-membership-plans-table.php +++ b/includes/hub/admin/class-membership-plans-table.php @@ -15,23 +15,6 @@ * The Membership_Plans Table */ class Membership_Plans_Table extends \WP_List_Table { - /** - * Whether to show local or network plans. - * - * @var bool - */ - private $is_local = false; - - /** - * Constructs the controller. - * - * @param bool $is_local Whether to show local or network plans. - */ - public function __construct( $is_local = false ) { - $this->is_local = $is_local; - parent::__construct(); - } - /** * Get the table columns * @@ -42,12 +25,12 @@ public function get_columns() { 'id' => __( 'ID', 'newspack-network' ), 'name' => __( 'Name', 'newspack-network' ), ]; - if ( $this->is_local ) { - $columns['node_url'] = __( '-', 'newspack-network' ); - } else { - $columns['node_url'] = __( 'Node URL', '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['links'] = __( 'Links', 'newspack-network' ); return $columns; } @@ -57,11 +40,7 @@ public function get_columns() { */ public function prepare_items() { $this->_column_headers = [ $this->get_columns(), [], [], 'id' ]; - if ( $this->is_local ) { - $this->items = Membership_Plans::get_local_membership_plans(); - } else { - $this->items = Membership_Plans::get_membershp_plans_from_nodes(); - } + $this->items = Membership_Plans::get_membershp_plans_from_network(); } /** @@ -72,16 +51,42 @@ public function prepare_items() { * @return string */ 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 === 'network_pass_id' && $item[ $column_name ] ) { return sprintf( '%s', $item[ $column_name ] ); } - if ( $column_name === 'links' ) { - $edit_url = get_edit_post_link( $item['id'] ); - if ( isset( $item['node_url'] ) ) { - $edit_url = sprintf( '%s/wp-admin/post.php?post=%d&action=edit', $item['node_url'], $item['id'] ); + if ( $column_name === 'network_pass_discrepancies' && isset( $item['network_pass_discrepancies'] ) && $item['network_pass_id'] ) { + $discrepancies = $item['network_pass_discrepancies']; + $count = count( $discrepancies ); + if ( $count === 0 ) { + return esc_html__( 'None', 'newspack-network' ); } + + $memberships_list_url_with_emails_url = add_query_arg( + \Newspack_Network\Woocommerce_Memberships\Admin::MEMBERSHIPS_TABLE_EMAILS_QUERY_PARAM, + implode( ',', $discrepancies ), + $memberships_list_url + ); + $message = sprintf( + /* translators: %d is the number of members */ + _n( + '%d member doesn\'t match the shared member pool', + '%d members don\'t match the shared member pool', + $count, + 'newspack-plugin' + ), + $count + ); + return sprintf( '%s', 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( '%s', esc_url( $edit_url ), esc_html__( 'Edit', 'newspack-network' ) ); } + if ( $column_name === 'active_members_count' && $item[ $column_name ] ) { + return sprintf( '%s', esc_url( $memberships_list_url ), $item[ $column_name ] ); + } return isset( $item[ $column_name ] ) ? $item[ $column_name ] : ''; } } diff --git a/includes/hub/admin/class-membership-plans.php b/includes/hub/admin/class-membership-plans.php index 3f716841..6f4bc570 100644 --- a/includes/hub/admin/class-membership-plans.php +++ b/includes/hub/admin/class-membership-plans.php @@ -43,18 +43,12 @@ public static function render() { ?>
-

+

prepare_items(); $table->display(); ?> -

- prepare_items(); - $table->display(); - ?>

@@ -84,6 +78,9 @@ public static function render() { */ 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 ); + } $response = wp_remote_get( // phpcs:ignore $endpoint, [ @@ -107,12 +104,24 @@ private static function get_membership_plans_from_cache() { /** * Get membership plans from all nodes. */ - public static function get_membershp_plans_from_nodes() { + public static function get_membershp_plans_from_network() { $plans_cache = self::get_membership_plans_from_cache(); if ( $plans_cache && isset( $plans_cache['plans'] ) ) { return $plans_cache['plans']; } + $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']; + } + } + $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' ); @@ -123,14 +132,44 @@ public static function get_membershp_plans_from_nodes() { $network_pass_id = $meta->value; } } + if ( $network_pass_id && Network_Admin::use_experimental_auditing_features() ) { + if ( ! isset( $by_network_pass_id[ $network_pass_id ] ) ) { + $by_network_pass_id[ $network_pass_id ] = []; + } + $by_network_pass_id[ $network_pass_id ][ $node->get_url() ] = $plan->active_members_emails; + } $membership_plans[] = [ - 'id' => $plan->id, - 'node_url' => $node->get_url(), - 'name' => $plan->name, - 'network_pass_id' => $network_pass_id, + 'id' => $plan->id, + 'site_url' => $node->get_url(), + 'name' => $plan->name, + 'network_pass_id' => $network_pass_id, + 'active_members_count' => $plan->active_members_count, ]; } } + + if ( Network_Admin::use_experimental_auditing_features() ) { + $discrepancies = []; + foreach ( $by_network_pass_id as $plan_network_pass_id => $by_site ) { + $shared_emails = array_intersect( ...array_values( $by_site ) ); + foreach ( $by_site as $site_url => $emails ) { + $discrepancies[ $plan_network_pass_id ][ $site_url ] = array_diff( $emails, $shared_emails ); + } + } + $membership_plans = array_map( + function( $plan ) use ( $discrepancies ) { + if ( isset( + $plan['network_pass_id'], + $discrepancies[ $plan['network_pass_id'] ], + $discrepancies[ $plan['network_pass_id'] ][ $plan['site_url'] ] + ) ) { + $plan['network_pass_discrepancies'] = $discrepancies[ $plan['network_pass_id'] ][ $plan['site_url'] ]; + } + return $plan; + }, + $membership_plans + ); + } $plans_to_save = [ 'plans' => $membership_plans, 'last_updated' => time(), @@ -148,11 +187,17 @@ public static function get_local_membership_plans() { return []; } foreach ( wc_memberships_get_membership_plans() as $plan ) { - $membership_plans[] = [ - 'id' => $plan->post->ID, - 'name' => $plan->post->post_title, - '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' ), ]; + if ( Network_Admin::use_experimental_auditing_features() ) { + $plan_data['active_members_emails'] = \Newspack_Network\Woocommerce_Memberships\Admin::get_active_members_emails( $plan ); + } + $membership_plans[] = $plan_data; } return $membership_plans; } diff --git a/includes/hub/admin/class-nodes-list.php b/includes/hub/admin/class-nodes-list.php index 1aedd2a2..d02d43f5 100644 --- a/includes/hub/admin/class-nodes-list.php +++ b/includes/hub/admin/class-nodes-list.php @@ -36,6 +36,18 @@ public static function init() { public static function posts_columns( $columns ) { unset( $columns['date'] ); unset( $columns['stats'] ); + if ( \Newspack_Network\Admin::use_experimental_auditing_features() ) { + $sync_users_info = sprintf( + ' ', + sprintf( + /* translators: list of user roles which will entail synchronization */ + esc_attr__( 'Users with the following roles: %1$s (%2$d on the Hub)', 'newspack-network' ), + implode( ', ', \Newspack_Network\Utils\Users::get_synced_user_roles() ), + \Newspack_Network\Utils\Users::get_synchronized_users_count() + ) + ); + $columns['sync_users'] = __( 'Synchronizable Users', 'newspack-network' ) . $sync_users_info; + } $columns['links'] = __( 'Links', 'newspack-network' ); return $columns; } @@ -48,8 +60,8 @@ public static function posts_columns( $columns ) { * @return void */ public static function posts_columns_values( $column, $post_id ) { + $node = new Node( $post_id ); if ( 'links' === $column ) { - $node = new Node( $post_id ); $links = array_map( function ( $bookmark ) { return sprintf( '%s', esc_url( $bookmark['url'] ), esc_html( $bookmark['label'] ) ); @@ -68,6 +80,17 @@ function ( $bookmark ) {

implode( ',', \Newspack_Network\Utils\Users::get_synced_user_roles() ), + ], + trailingslashit( $node->get_url() ) . 'wp-admin/users.php' + ); + ?> + get_sync_users_count() ); ?> + get_node_url(), + $item->get_node_url(), $item->get_remote_id() ); printf( '%s', $link, $item->get_payment_count() ); // phpcs:ignore @@ -113,4 +120,56 @@ public static function posts_columns_values( $column, $post_id ) { break; } } + + /** + * Add sortable columns. + * + * @param array $columns Columns. + * @return array + */ + public static function subscription_sortable_columns( $columns ) { + $sortable_columns = [ + 'start_date' => 'start_date', + 'trial_end_date' => 'trial_end_date', + 'next_payment_date' => 'next_payment_date', + 'last_payment_date' => 'last_payment_date', + 'end_date' => 'end_date', + ]; + + return wp_parse_args( $sortable_columns, $columns ); + } + + /** + * Sorts the request for subscriptions stored in WP Post tables. + * + * @param array $vars Query variables. + * + * @return array + */ + public static function request_query( $vars ) { + global $typenow; + + if ( \Newspack_Network\Hub\Database\Subscriptions::POST_TYPE_SLUG === $typenow ) { + if ( isset( $vars['orderby'] ) ) { + switch ( $vars['orderby'] ) { + case 'start_date': + case 'trial_end_date': + case 'next_payment_date': + case 'last_payment_date': + case 'end_date': + $vars = array_merge( + $vars, + [ + 'meta_key' => $vars['orderby'], + 'meta_type' => 'DATETIME', + 'orderby' => 'meta_value', + ] + ); + break; + } + } + } + + return $vars; + } } diff --git a/includes/hub/admin/class-users.php b/includes/hub/admin/class-users.php deleted file mode 100644 index bb21f554..00000000 --- a/includes/hub/admin/class-users.php +++ /dev/null @@ -1,78 +0,0 @@ - $user->user_email ], 1 ); - - if ( empty( $last_activity ) ) { - return '-'; - } - - $last_activity = $last_activity[0]; - - $summary = $last_activity->get_summary(); - $event_log_url = add_query_arg( - [ - 'page' => Event_Log::PAGE_SLUG, - 'email' => $user->user_email, - ], - admin_url( 'admin.php' ) - ); - return sprintf( - '%s: %s
%s', - __( 'Last Activity', 'newspack-network' ), - $summary, - $event_log_url, - __( 'View all', 'newspack-network' ) - ); - - } - return $value; - } -} diff --git a/includes/hub/admin/class-woo.php b/includes/hub/admin/class-woo.php index 6ebbe860..082417cf 100644 --- a/includes/hub/admin/class-woo.php +++ b/includes/hub/admin/class-woo.php @@ -39,7 +39,7 @@ public static function init() { $class_name = get_called_class(); $db_class_name = str_replace( 'Admin', 'Database', $class_name ); self::$post_types[] = $db_class_name::POST_TYPE_SLUG; - + // Removes the Bulk actions dropdown. add_filter( 'bulk_actions-edit-' . $db_class_name::POST_TYPE_SLUG, '__return_empty_array' ); @@ -158,7 +158,7 @@ public static function pre_get_posts( $query ) { if ( 'edit.php' !== $pagenow || ! in_array( $post_type, self::$post_types, true ) || ! is_admin() || ! $query->is_main_query() ) { return null; } - + if ( ! isset( $_GET['node_id'] ) || ! is_numeric( $_GET['node_id'] ) ) { // zero is a valid value. return null; } @@ -178,7 +178,6 @@ public static function pre_get_posts( $query ) { */ public static function parse_query( $query ) { global $pagenow; - if ( ! is_admin() || 'edit.php' !== $pagenow || empty( $query->query_vars['s'] ) || ! in_array( $query->query_vars['post_type'], self::$post_types, true ) ) { return; } diff --git a/includes/hub/admin/css/nodes-list.css b/includes/hub/admin/css/nodes-list.css index 414a4737..8c3596bc 100644 --- a/includes/hub/admin/css/nodes-list.css +++ b/includes/hub/admin/css/nodes-list.css @@ -2,4 +2,8 @@ .links.column-links { width: 40%; text-align: left; +} +.dashicons-info-outline{ + cursor: help; + opacity: 0.8; } \ No newline at end of file diff --git a/includes/hub/class-admin.php b/includes/hub/class-admin.php index 7e129300..e0df5b64 100644 --- a/includes/hub/class-admin.php +++ b/includes/hub/class-admin.php @@ -20,7 +20,6 @@ public static function init() { Admin\Subscriptions::init(); Admin\Orders::init(); Admin\Membership_Plans::init(); - Admin\Users::init(); Admin\Nodes_List::init(); Distributor_Settings::init(); } diff --git a/includes/hub/class-node.php b/includes/hub/class-node.php index fb23cae4..f50d1867 100644 --- a/includes/hub/class-node.php +++ b/includes/hub/class-node.php @@ -170,4 +170,25 @@ public function get_bookmarks() { return self::generate_bookmarks( $base_url ); } + + /** + * Get site info. + */ + private function get_site_info() { + $response = wp_remote_get( // phpcs:ignore + $this->get_url() . '/wp-json/newspack-network/v1/info', + [ + 'headers' => $this->get_authorization_headers( 'info' ), + ] + ); + return json_decode( wp_remote_retrieve_body( $response ) ); + } + + /** + * Get synchronized users count. + */ + public function get_sync_users_count() { + $site_info = $this->get_site_info(); + return $site_info->sync_users_count ?? 0; + } } diff --git a/includes/node/class-info-endpoints.php b/includes/node/class-info-endpoints.php new file mode 100644 index 00000000..b5b57238 --- /dev/null +++ b/includes/node/class-info-endpoints.php @@ -0,0 +1,53 @@ + \WP_REST_Server::READABLE, + 'callback' => [ __CLASS__, 'handle_info_request' ], + 'permission_callback' => '__return_true', + ], + ] + ); + } + + /** + * Handles the info request. + */ + public static function handle_info_request() { + return rest_ensure_response( + [ + 'sync_users_count' => \Newspack_Network\Utils\Users::get_synchronized_users_count(), + ] + ); + } +} diff --git a/includes/utils/class-users.php b/includes/utils/class-users.php index 270aeaa7..cb88ef91 100644 --- a/includes/utils/class-users.php +++ b/includes/utils/class-users.php @@ -114,4 +114,28 @@ public static function maybe_sideload_avatar( $user_id, $user_data, $overwrite ) Debugger::log( 'No avatar found in user data' ); return false; } + + /** + * Get synchronization-entailing user roles. + */ + public static function get_synced_user_roles() { + if ( ! method_exists( '\Newspack\Reader_Activation', 'get_reader_roles' ) ) { + return []; + } + return \Newspack\Reader_Activation::get_reader_roles(); + } + + /** + * Get synchronized users count. + */ + public static function get_synchronized_users_count() { + $users = get_users( + [ + 'role__in' => self::get_synced_user_roles(), + 'fields' => [ 'id' ], + 'number' => -1, + ] + ); + return count( $users ); + } } diff --git a/includes/woocommerce-memberships/class-admin.php b/includes/woocommerce-memberships/class-admin.php index 0212c758..38a752e9 100644 --- a/includes/woocommerce-memberships/class-admin.php +++ b/includes/woocommerce-memberships/class-admin.php @@ -49,6 +49,13 @@ class Admin { */ const SITE_URL_META_KEY = '_remote_site_url'; + /** + * The special param to filter the WC Memberships table. + * + * @var string + */ + const MEMBERSHIPS_TABLE_EMAILS_QUERY_PARAM = '_newspack_emails_query'; + /** * Initializer. */ @@ -58,6 +65,43 @@ public static function init() { add_filter( 'get_edit_post_link', array( __CLASS__, 'get_edit_post_link' ), 10, 2 ); 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( 'request', [ __CLASS__, 'request_query' ] ); + add_action( 'pre_user_query', [ __CLASS__, 'pre_user_query' ] ); + add_action( 'admin_notices', [ __CLASS__, 'admin_notices' ] ); + } + + /** + * Get active members' emails. + * + * @param \WC_Memberships_Membership_Plan $plan the membership plan. + */ + public static function get_active_members_emails( $plan ) { + $active_memberships = $plan->get_memberships( [ 'post_status' => 'wcm-active' ] ); + return array_map( + function ( $membership ) { + $user = get_user_by( 'id', $membership->get_user_id() ); + return $user->user_email; + }, + $active_memberships + ); + } + + /** + * Filter membership plans to add user count. + * + * @param array $data associative array of membership plan data. + * @param \WC_Memberships_Membership_Plan $plan the membership plan. + * @param null|\WP_REST_Request $request The request object. + */ + 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 ); + } + } + return $data; } /** @@ -184,4 +228,80 @@ public static function map_meta_cap( $caps, $cap, $user_id, $args ) { } return $caps; } + + /** + * Get table email query param. + */ + private static function get_table_emails_query_param() { + $emails_param_value = isset( $_GET[ self::MEMBERSHIPS_TABLE_EMAILS_QUERY_PARAM ] ) ? sanitize_text_field( $_GET[ self::MEMBERSHIPS_TABLE_EMAILS_QUERY_PARAM ] ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( $emails_param_value ) { + return explode( ',', $emails_param_value ); + } + return false; + } + + /** + * Handles custom filters for the user memberships screen. + * + * @param array $vars query vars for \WP_Query. + * @return array modified query vars. + */ + public static function request_query( $vars ) { + global $typenow; + if ( 'wc_user_membership' === $typenow ) { + if ( self::get_table_emails_query_param() ) { + $users = get_users( + [ + 'fields' => 'ID', + 'search' => 'emails', + 'search_columns' => [ 'user_email' ], + ] + ); + $vars['author__in'] = $users; + } + } + return $vars; + } + + /** + * Handles custom filters for the user query. + * + * @param \WP_User_Query $user_query The user query. + */ + public static function pre_user_query( $user_query ) { + $emails_from_query = self::get_table_emails_query_param(); + if ( $emails_from_query ) { + $emails = array_map( + function( $email ) { + return "'$email'"; + }, + $emails_from_query + ); + $user_query->query_where = preg_replace( + "/user_email LIKE 'emails'/", + 'user_email LIKE ' . implode( ' OR user_email LIKE ', $emails ), + $user_query->query_where + ); + } + } + + /** + * Admin notice if viewing memberships table with emails filter. + */ + public static function admin_notices() { + $emails_from_query = self::get_table_emails_query_param(); + if ( $emails_from_query ) { + ?> +
+

+ +

+
+ +