diff --git a/includes/class-users.php b/includes/class-users.php index d3d48d3b..16fa3507 100644 --- a/includes/class-users.php +++ b/includes/class-users.php @@ -7,6 +7,8 @@ namespace Newspack_Network; +use const Newspack_Network\constants\EVENT_LOG_PAGE_SLUG; + /** * Class to handle the Users admin page */ @@ -75,7 +77,7 @@ public static function manage_users_custom_column( $value, $column_name, $user_i $summary = $last_activity->get_summary(); $event_log_url = add_query_arg( [ - 'page' => \Newspack_Network\Hub\Admin\Event_Log::PAGE_SLUG, + 'page' => EVENT_LOG_PAGE_SLUG, 'email' => $user->user_email, ], admin_url( 'admin.php' ) diff --git a/includes/cli/class-membership-dedupe.php b/includes/cli/class-membership-dedupe.php index 1b9a3398..9d209e2d 100644 --- a/includes/cli/class-membership-dedupe.php +++ b/includes/cli/class-membership-dedupe.php @@ -39,7 +39,7 @@ public static function register_commands() { [ 'type' => 'assoc', 'name' => 'plan-id', - 'optional' => false, + 'optional' => true, ], [ 'type' => 'flag', @@ -64,62 +64,97 @@ public static function register_commands() { * * wp newspack-network clean-up-duplicate-memberships --plan-id=1234 * + * ## OPTIONS + * + * [--plan-id] + * : Membership plan ID to clean up. If not set, all synchronized plans will be cleaned up. + * + * [--live] + * : Run the command in live mode, updating the users. + * + * [--csv] + * : Output CSV. + * * @param array $args Positional args. * @param array $assoc_args Associative args and flags. */ public static function clean_duplicate_memberships( $args, $assoc_args ) { WP_CLI::line( '' ); - $live = isset( $assoc_args['live'] ); - $csv = isset( $assoc_args['csv'] ); - - $plan_id = $assoc_args['plan-id']; - if ( ! is_numeric( $plan_id ) ) { - WP_CLI::error( 'Membership plan ID must be numeric' ); - } - $plan_id = (int) $plan_id; + $live = isset( $assoc_args['live'] ); if ( ! $live ) { WP_CLI::line( 'Running in dry-run mode. Use --live flag to run in live mode.' ); WP_CLI::line( '' ); } - $user_ids = self::get_users_with_duplicate_membership( $plan_id ); - WP_CLI::line( sprintf( '%d users found with duplicate memberships', count( $user_ids ) ) ); + $csv = isset( $assoc_args['csv'] ); - $duplicates = []; - foreach ( $user_ids as $user_id ) { - $memberships = get_posts( + $plan_id_from_args = isset( $assoc_args['plan-id'] ) ? $assoc_args['plan-id'] : null; + $plan_ids = []; + if ( $plan_id_from_args ) { + if ( ! is_numeric( $plan_id_from_args ) ) { + WP_CLI::error( 'Membership plan ID must be numeric' ); + } + $plan_ids = [ (int) $plan_id_from_args ]; + } else { + // Get all network-sync'd membership plans. + $plan_ids = get_posts( [ - 'author' => $user_id, - 'post_type' => 'wc_user_membership', - 'post_status' => 'any', - 'post_parent' => $plan_id, + 'post_type' => 'wc_membership_plan', + 'fields' => 'ids', + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, + 'compare' => 'EXISTS', + ], + ], ] ); + } - foreach ( $memberships as $membership ) { - $user = get_user_by( 'id', $membership->post_author ); - $duplicates[] = [ - 'user' => $membership->post_author, - 'email' => $user->user_email, - 'membership' => $membership->ID, - 'subscription' => get_post_meta( $membership->ID, '_subscription_id', true ), - 'status' => $membership->post_status, - 'remote' => get_post_meta( $membership->ID, '_remote_site_url', true ), - ]; + $user_ids = []; + foreach ( $plan_ids as $plan_id ) { + WP_CLI::line( sprintf( 'Checking plan #%d', $plan_id ) ); + + $user_ids = array_merge( $user_ids, self::get_users_with_duplicate_membership( $plan_id ) ); + WP_CLI::line( sprintf( '%d users found with duplicate memberships', count( $user_ids ) ) ); + + $duplicates = []; + foreach ( $user_ids as $user_id ) { + $memberships = get_posts( + [ + 'author' => $user_id, + 'post_type' => 'wc_user_membership', + 'post_status' => 'any', + 'post_parent' => $plan_id, + ] + ); + + foreach ( $memberships as $membership ) { + $user = get_user_by( 'id', $membership->post_author ); + if ( $user === false ) { + continue; + } + $duplicates[] = [ + 'user' => $membership->post_author, + 'email' => $user->user_email, + 'membership' => $membership->ID, + 'subscription' => get_post_meta( $membership->ID, '_subscription_id', true ), + 'status' => $membership->post_status, + 'remote' => get_post_meta( $membership->ID, '_remote_site_url', true ), + ]; + } } - } - if ( $csv && ! empty( $duplicates ) ) { - WP_CLI::line( 'COPY AND PASTE THIS CSV: ' ); - WP_CLI::line(); - WP_CLI\Utils\format_items( 'csv', $duplicates, array_keys( $duplicates[0] ) ); - WP_CLI::line(); - } + if ( $csv && ! empty( $duplicates ) ) { + WP_CLI::line( 'COPY AND PASTE THIS CSV: ' ); + WP_CLI::line(); + WP_CLI\Utils\format_items( 'csv', $duplicates, array_keys( $duplicates[0] ) ); + WP_CLI::line(); + } - if ( $live ) { - WP_CLI::line( 'Deleting duplicates' ); - self::deduplicate_memberships( $duplicates ); + self::deduplicate_memberships( $duplicates, $live ); + WP_CLI::line( '' ); } WP_CLI::success( 'Done' ); @@ -151,8 +186,12 @@ private static function get_users_with_duplicate_membership( $plan_id ) { * De-duplicate memberships so that users only have one membership of a plan. * * @param array $duplicates Analyzed data from ::clean_duplicate_memberships. + * @param bool $live Whether to actually delete the duplicates. */ - private static function deduplicate_memberships( $duplicates ) { + private static function deduplicate_memberships( $duplicates, $live ) { + if ( $live ) { + WP_CLI::line( 'Deleting duplicates' ); + } $userdata = []; foreach ( $duplicates as $duplicate ) { @@ -166,13 +205,17 @@ private static function deduplicate_memberships( $duplicates ) { foreach ( $userdata as $email => $duplicates ) { WP_CLI::line( sprintf( 'Processing %s', $email ) ); if ( count( $duplicates ) < 2 ) { - WP_CLI::line( ' - User does not have too many memberships' ); + WP_CLI::line( ' - User has multiple memberships, but no duplicates' ); } $memberships_to_delete = array_slice( $duplicates, 1 ); foreach ( $memberships_to_delete as $duplicate ) { - wp_delete_post( $duplicate['membership'], true ); - WP_CLI::line( sprintf( ' - Deleted extra membership %d', $duplicate['membership'] ) ); + if ( $live ) { + wp_delete_post( $duplicate['membership'], true ); + WP_CLI::line( sprintf( ' - Deleted extra membership %d', $duplicate['membership'] ) ); + } else { + WP_CLI::line( sprintf( ' - Would have deleted extra membership %d', $duplicate['membership'] ) ); + } } } } diff --git a/includes/constants.php b/includes/constants.php index f0bc43b9..b3e96cd7 100644 --- a/includes/constants.php +++ b/includes/constants.php @@ -1,7 +1,7 @@ 'Invalid Signature.', 'INVALID_DATA' => 'Bad request. Invalid Data.', ]; + +const EVENT_LOG_PAGE_SLUG = 'newspack-network-event-log'; diff --git a/includes/hub/admin/class-event-log.php b/includes/hub/admin/class-event-log.php index f7170018..cc68ff7c 100644 --- a/includes/hub/admin/class-event-log.php +++ b/includes/hub/admin/class-event-log.php @@ -8,14 +8,13 @@ namespace Newspack_Network\Hub\Admin; use Newspack_Network\Admin as Network_Admin; +use const Newspack_Network\constants\EVENT_LOG_PAGE_SLUG; /** * Class to handle the Event log admin page */ class Event_Log { - const PAGE_SLUG = 'newspack-network-event-log'; - /** * Runs the initialization. */ @@ -30,7 +29,7 @@ public static function init() { * @return void */ public static function add_admin_menu() { - Network_Admin::add_submenu_page( __( 'Event Log', 'newspack-network' ), self::PAGE_SLUG, [ __CLASS__, 'render_page' ] ); + Network_Admin::add_submenu_page( __( 'Event Log', 'newspack-network' ), EVENT_LOG_PAGE_SLUG, [ __CLASS__, 'render_page' ] ); } /** @@ -39,7 +38,7 @@ public static function add_admin_menu() { * @return void */ public static function admin_enqueue_scripts() { - $page_slug = Network_Admin::PAGE_SLUG . '_page_' . self::PAGE_SLUG; + $page_slug = Network_Admin::PAGE_SLUG . '_page_' . EVENT_LOG_PAGE_SLUG; if ( get_current_screen()->id !== $page_slug ) { return; } @@ -62,7 +61,7 @@ public static function render_page() { echo '