diff --git a/includes/class-accepted-actions.php b/includes/class-accepted-actions.php
index bf4071d8..3457883a 100644
--- a/includes/class-accepted-actions.php
+++ b/includes/class-accepted-actions.php
@@ -35,6 +35,7 @@ class Accepted_Actions {
'donation_new' => 'Donation_New',
'donation_subscription_cancelled' => 'Donation_Subscription_Cancelled',
'network_user_updated' => 'User_Updated',
+ 'network_user_deleted' => 'User_Deleted',
'newspack_network_woo_membership_updated' => 'Woocommerce_Membership_Updated',
'network_manual_sync_user' => 'User_Manually_Synced',
'network_nodes_synced' => 'Nodes_Synced',
@@ -53,6 +54,7 @@ class Accepted_Actions {
'donation_new',
'donation_subscription_cancelled',
'network_user_updated',
+ 'network_user_deleted',
'newspack_network_woo_membership_updated',
'network_manual_sync_user',
'network_nodes_synced',
diff --git a/includes/class-admin.php b/includes/class-admin.php
index 5ef77d1a..20abf1ed 100644
--- a/includes/class-admin.php
+++ b/includes/class-admin.php
@@ -198,11 +198,6 @@ public static function enqueue_scripts() {
* @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;
+ return defined( 'NEWSPACK_NETWORK_EXPERIMENTAL_AUDITING_FEATURES' ) ? NEWSPACK_NETWORK_EXPERIMENTAL_AUDITING_FEATURES : false;
}
}
diff --git a/includes/class-data-listeners.php b/includes/class-data-listeners.php
index 6468c612..64cb0b86 100644
--- a/includes/class-data-listeners.php
+++ b/includes/class-data-listeners.php
@@ -36,6 +36,7 @@ public static function register_listeners() {
Data_Events::register_listener( 'woocommerce_subscription_status_changed', 'newspack_node_subscription_changed', [ __CLASS__, 'item_changed' ] );
Data_Events::register_listener( 'woocommerce_order_status_changed', 'newspack_node_order_changed', [ __CLASS__, 'item_changed' ] );
Data_Events::register_listener( 'newspack_network_user_updated', 'network_user_updated', [ __CLASS__, 'user_updated' ] );
+ Data_Events::register_listener( 'delete_user', 'network_user_deleted', [ __CLASS__, 'user_deleted' ] );
Data_Events::register_listener( 'newspack_network_nodes_synced', 'network_nodes_synced', [ __CLASS__, 'nodes_synced' ] );
}
@@ -87,6 +88,26 @@ public static function user_updated( $user_data ) {
return $user_data;
}
+ /**
+ * Filters the user data for the event being triggered
+ *
+ * @param int $id ID of the user to delete.
+ * @param int|null $reassign ID of the user to reassign posts and links to.
+ * Default null, for no reassignment.
+ * @param WP_User $user WP_User object of the user to delete.
+ * @return array
+ */
+ public static function user_deleted( $id, $reassign, $user ) {
+ $should_delete = apply_filters( 'newspack_network_process_user_deleted', true, $user->user_email );
+ if ( ! $should_delete ) {
+ Debugger::log( 'User deletion with email: ' . $user->user_email . ' was skipped due to filter use.' );
+ return;
+ }
+ return [
+ 'email' => $user->user_email,
+ ];
+ }
+
/**
* Filters the nodes data for the event being triggered
*
diff --git a/includes/class-esp-metadata-sync.php b/includes/class-esp-metadata-sync.php
index 20cabc3b..9f7f599e 100644
--- a/includes/class-esp-metadata-sync.php
+++ b/includes/class-esp-metadata-sync.php
@@ -7,6 +7,8 @@
namespace Newspack_Network;
+use Newspack_Network\Utils\Users as User_Utils;
+
/**
* Class to handle Node settings page
*/
@@ -21,6 +23,8 @@ public static function init() {
\add_filter( 'newspack_ras_metadata_keys', [ __CLASS__, 'add_custom_metadata_fields' ] );
\add_filter( 'newspack_register_reader_metadata', [ __CLASS__, 'handle_custom_metadata_fields' ], 10, 2 );
\add_filter( 'newspack_data_events_reader_registered_metadata', [ __CLASS__, 'handle_custom_metadata_fields' ], 10, 2 );
+ \add_action( 'newspack_network_network_reader', [ __CLASS__, 'handle_custom_metadata_for_network_readers' ] );
+ \add_action( 'newspack_network_new_network_reader', [ __CLASS__, 'handle_custom_metadata_for_network_readers' ] );
}
/**
@@ -48,9 +52,62 @@ public static function add_custom_metadata_fields( $metadata_fields ) {
*/
public static function handle_custom_metadata_fields( $metadata, $user_id ) {
if ( $user_id ) {
- $metadata['network_registration_site'] = \esc_url( \get_site_url() );
+ $remote_site = \get_user_meta( $user_id, User_Utils::USER_META_REMOTE_SITE, true );
+ $registration_site = \esc_url( ! empty( \wp_http_validate_url( $remote_site ) ) ? $remote_site : \get_site_url() );
+ $metadata['network_registration_site'] = $registration_site;
}
return $metadata;
}
+
+ /**
+ * Trigger a reader data sync to the connected ESP.
+ *
+ * @param array $contact The contact data to sync.
+ */
+ public static function sync_contact( $contact ) {
+ // Only if Reader Activation and Newspack Newsletters are available.
+ if ( ! class_exists( 'Newspack\Reader_Activation' ) || ! method_exists( 'Newspack_Newsletters', 'service_provider' ) ) {
+ return;
+ }
+
+ // Only if RAS + ESP sync is enabled.
+ if ( ! \Newspack\Reader_Activation::is_enabled() || ! \Newspack\Reader_Activation::get_setting( 'sync_esp' ) ) {
+ return;
+ }
+
+ // Only if we have the ESP Data Events connectors.
+ if ( ! class_exists( 'Newspack\Data_Events\Connectors\Mailchimp' ) || ! class_exists( 'Newspack\Data_Events\Connectors\ActiveCampaign' ) ) {
+ return;
+ }
+
+ $service_provider = \Newspack_Newsletters::service_provider();
+ if ( 'mailchimp' === $service_provider ) {
+ return \Newspack\Data_Events\Connectors\Mailchimp::put( $contact );
+ } elseif ( 'active_campaign' === $service_provider ) {
+ return \Newspack\Data_Events\Connectors\ActiveCampaign::put( $contact );
+ }
+ }
+
+ /**
+ * Sync custom metadata fields for network readers.
+ *
+ * @param WP_User $user The newly created or existing user.
+ */
+ public static function handle_custom_metadata_for_network_readers( $user ) {
+ if ( ! $user ) {
+ return;
+ }
+ $contact = \Newspack\WooCommerce_Connection::get_contact_from_customer( new \WC_Customer( $user->ID ) );
+ $metadata = $contact['metadata'] ?? [];
+
+ // Ensure email is set as the user probably won't have a billing email.
+ if ( ! isset( $contact['email'] ) ) {
+ $contact['email'] = $user->user_email;
+ }
+
+ $contact['metadata'] = self::handle_custom_metadata_fields( $metadata, $user->ID );
+
+ self::sync_contact( $contact );
+ }
}
diff --git a/includes/class-initializer.php b/includes/class-initializer.php
index 758e9687..7e78c80b 100644
--- a/includes/class-initializer.php
+++ b/includes/class-initializer.php
@@ -24,9 +24,12 @@ 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();
+ // phpcs:disable
+ // Hub\Database\Subscriptions::init();
+ // Hub\Database\Orders::init();
+ // phpcs:enable
Hub\Newspack_Ads_GAM::init();
Hub\Connect_Node::init();
}
@@ -53,6 +56,7 @@ public static function init() {
Synchronize_All::init();
Data_Backfill::init();
+ Misc::init();
Membership_Dedupe::init();
Woocommerce_Memberships\Admin::init();
diff --git a/includes/class-rest-authenticaton.php b/includes/class-rest-authenticaton.php
index 8cd11a80..3c223e5a 100644
--- a/includes/class-rest-authenticaton.php
+++ b/includes/class-rest-authenticaton.php
@@ -37,6 +37,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' ],
+ ],
];
/**
diff --git a/includes/class-users.php b/includes/class-users.php
index d3d48d3b..f5391d99 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
*/
@@ -17,7 +19,7 @@ class Users {
*/
public static function init() {
add_filter( 'manage_users_columns', [ __CLASS__, 'manage_users_columns' ] );
- add_filter( 'manage_users_custom_column', [ __CLASS__, 'manage_users_custom_column' ], 10, 3 );
+ add_filter( 'manage_users_custom_column', [ __CLASS__, 'manage_users_custom_column' ], 99, 3 ); // priority must be higher than Jetpack's jetpack_show_connection_status (10).
add_filter( 'users_list_table_query_args', [ __CLASS__, 'users_list_table_query_args' ] );
}
@@ -28,9 +30,7 @@ public static function init() {
* @return array
*/
public static function manage_users_columns( $columns ) {
- if ( Site_Role::is_hub() ) {
- $columns['newspack_network_activity'] = __( 'Newspack Network Activity', 'newspack-network' );
- }
+ $columns['newspack_network_activity'] = __( 'Newspack Network Activity', 'newspack-network' );
if ( \Newspack_Network\Admin::use_experimental_auditing_features() ) {
$columns['newspack_network_user'] = __( 'Network Original User', 'newspack-network' );
}
@@ -58,36 +58,45 @@ public static function manage_users_custom_column( $value, $column_name, $user_i
);
}
}
- if ( 'newspack_network_activity' === $column_name && Site_Role::is_hub() ) {
+ if ( 'newspack_network_activity' === $column_name ) {
$user = get_user_by( 'id', $user_id );
if ( ! $user ) {
return $value;
}
+ if ( Site_Role::is_hub() ) {
+ $last_activity = \Newspack_Network\Hub\Stores\Event_Log::get( [ 'email' => $user->user_email ], 1 );
+ if ( empty( $last_activity ) ) {
+ return '-';
+ }
- $last_activity = \Newspack_Network\Hub\Stores\Event_Log::get( [ 'email' => $user->user_email ], 1 );
-
- if ( empty( $last_activity ) ) {
- return '-';
+ $event_log_url = add_query_arg(
+ [
+ 'page' => EVENT_LOG_PAGE_SLUG,
+ 'email' => urlencode( $user->user_email ),
+ ],
+ admin_url( 'admin.php' )
+ );
+ return sprintf(
+ '%s: %s
%s',
+ __( 'Last Activity', 'newspack-network' ),
+ $last_activity[0]->get_summary(),
+ $event_log_url,
+ __( 'View all', 'newspack-network' )
+ );
+ } else {
+ $event_log_url = add_query_arg(
+ [
+ 'page' => EVENT_LOG_PAGE_SLUG,
+ 'email' => urlencode( $user->user_email ),
+ ],
+ untrailingslashit( Node\Settings::get_hub_url() ) . '/wp-admin/admin.php'
+ );
+ return sprintf(
+ '%s',
+ $event_log_url,
+ __( 'View activity', 'newspack-network' )
+ );
}
-
- $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;
}
@@ -104,6 +113,10 @@ public static function users_list_table_query_args( $args ) {
$args['role__in'] = explode( ',', sanitize_text_field( $_REQUEST['role__in'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
unset( $args['role'] );
}
+ if ( isset( $_REQUEST['role__not_in'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
+ $args['role__not_in'] = explode( ',', sanitize_text_field( $_REQUEST['role__not_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 7d13a1ea..4e291d30 100644
--- a/includes/cli/backfillers/class-reader-registered.php
+++ b/includes/cli/backfillers/class-reader-registered.php
@@ -35,7 +35,7 @@ public function get_events() {
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.
+ // Get all users registered between specified dates.
$users = get_users(
[
'role__in' => $roles_to_sync,
@@ -44,6 +44,7 @@ public function get_events() {
'before' => $this->end,
'inclusive' => true,
],
+ 'orderby' => 'user_registered',
'fields' => [ 'id', 'user_email', 'user_registered' ],
'number' => -1,
'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
@@ -55,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 ) );
@@ -76,9 +79,12 @@ function( $args ) {
foreach ( $users as $user ) {
$registration_method = get_user_meta( $user->ID, \Newspack\Reader_Activation::REGISTRATION_METHOD, true );
$user_data = [
- 'user_id' => $user->ID,
- 'email' => $user->user_email,
- 'metadata' => [
+ 'user_id' => $user->ID,
+ 'email' => $user->user_email,
+ 'user_registered' => $user->user_registered,
+ 'first_name' => get_user_meta( $user->ID, 'first_name', true ),
+ 'last_name' => get_user_meta( $user->ID, 'last_name', true ),
+ 'meta_input' => [
// 'current_page_url' is not saved, can't be backfilled.
'registration_method' => empty( $registration_method ) ? 'backfill-script' : $registration_method,
],
diff --git a/includes/cli/backfillers/class-woocommerce-membership-updated.php b/includes/cli/backfillers/class-woocommerce-membership-updated.php
index 1939654d..5b3d08c6 100644
--- a/includes/cli/backfillers/class-woocommerce-membership-updated.php
+++ b/includes/cli/backfillers/class-woocommerce-membership-updated.php
@@ -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 );
@@ -77,10 +80,21 @@ public function get_events() {
'membership_id' => $membership->get_id(),
'new_status' => $status,
];
- if ( $status === 'active' ) {
+ $timestamp = null;
+ switch ( $status ) {
+ case 'paused':
+ $timestamp = strtotime( $membership->get_paused_date() );
+ break;
+ case 'cancelled':
+ $timestamp = strtotime( $membership->get_cancelled_date() );
+ break;
+ case 'expired':
+ $timestamp = strtotime( $membership->get_end_date() );
+ break;
+ }
+
+ if ( ! $timestamp ) {
$timestamp = strtotime( $membership->get_start_date() );
- } else {
- $timestamp = strtotime( $membership->get_end_date() );
}
$events[] = new \Newspack_Network\Incoming_Events\Woocommerce_Membership_Updated( get_bloginfo( 'url' ), $membership_data, $timestamp );
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/cli/class-misc.php b/includes/cli/class-misc.php
new file mode 100644
index 00000000..967ca0cb
--- /dev/null
+++ b/includes/cli/class-misc.php
@@ -0,0 +1,821 @@
+ [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
+ 'relation' => 'OR',
+ [
+ 'key' => 'wp_capabilities',
+ 'compare' => 'NOT EXISTS',
+ ],
+ [
+ 'key' => 'wp_capabilities',
+ 'value' => 'a:0:{}',
+ 'compare' => '=',
+ ],
+ ],
+ ]
+ );
+ }
+ WP_CLI::line( 'Got ' . count( $users_to_update ) . ' users to update.' );
+ WP_CLI::line( '' );
+
+ foreach ( $users_to_update as $user ) {
+ // If read from CSV, find the user by email.
+ if ( is_array( $user ) ) {
+ $user_email = $user['user_email'];
+ $user = get_user_by( 'email', $user_email );
+ if ( $user === false ) {
+ WP_CLI::warning( 'User not found by email: ' . $user_email );
+ continue;
+ }
+ }
+ $user_id = $user->ID;
+ $role = 'subscriber';
+ // If the user has the newspack_remote_site meta, it's a network user.
+ $remote_site = get_user_meta( $user_id, \Newspack_Network\Utils\Users::USER_META_REMOTE_SITE, true );
+ if ( $remote_site ) {
+ $role = NEWSPACK_NETWORK_READER_ROLE;
+ }
+ if ( $live ) {
+ $user->set_role( $role );
+ WP_CLI::line( "š Assigned '$role' role to user $user->user_email (#$user_id)." );
+ } else {
+ WP_CLI::line( "š In live mode, would assign '$role' role to user $user->user_email (#$user_id)." );
+ }
+ }
+
+ WP_CLI::line( '' );
+ }
+
+ /**
+ * Get user memberships.
+ *
+ * @param array $args Indexed array of args.
+ * @param array $assoc_args Associative array of args.
+ * @return void
+ *
+ * ## EXAMPLES
+ *
+ * wp newspack-network get-user-memberships member-name@example.com
+ */
+ public static function get_user_memberships( array $args, array $assoc_args ) {
+ WP_CLI::line( '' );
+
+ $email_address = isset( $args[0] ) ? $args[0] : false;
+ if ( ! $email_address ) {
+ WP_CLI::error( 'Please provide an email address.' );
+ }
+
+ $memberships = \wc_memberships_get_user_memberships( get_user_by( 'email', $email_address ) );
+ foreach ( $memberships as $membership ) {
+ $plan_name = $membership->get_plan()->get_name();
+ $plan_network_id = get_post_meta( $membership->get_plan()->get_id(), \Newspack_Network\Woocommerce_Memberships\Admin::NETWORK_ID_META_KEY, true );
+ if ( $plan_network_id ) {
+ $plan_name .= ' (Network ID: ' . $plan_network_id . ')';
+ }
+
+ WP_CLI::line( 'ā” Membership ID: ' . $membership->get_id() . ', status: ' . $membership->get_status() . ', plan: ' . $plan_name );
+ $subscription = ( new \WC_Memberships_Integration_Subscriptions_User_Membership( $membership->post ) )->get_subscription();
+ if ( $subscription ) {
+ WP_CLI::line( sprintf( ' ā” linked with %s subscription #%d for amount: %d', $subscription->get_status(), $subscription->get_id(), $subscription->get_total() ) );
+ }
+ }
+
+ WP_CLI::line( '' );
+ }
+
+ /**
+ * Fix registration-based memberships. All registered users should have a membership if it's
+ * set to be created on registration.
+ *
+ * @param array $args Indexed array of args.
+ * @param array $assoc_args Associative array of args.
+ * @return void
+ *
+ * ## OPTIONS
+ *
+ * [--live]
+ * : Run the command in live mode, updating the users.
+ *
+ * [--verbose]
+ * : More output.
+ *
+ * ## EXAMPLES
+ *
+ * wp newspack-network fix-registration-based-memberships
+ */
+ public static function fix_registration_based_memberships( array $args, array $assoc_args ) {
+ WP_CLI::line( '' );
+
+ $live = isset( $assoc_args['live'] ) ? true : false;
+ $verbose = isset( $assoc_args['verbose'] ) ? true : false;
+ if ( $live ) {
+ WP_CLI::line( 'Live mode ā memberships will be created.' );
+ } else {
+ WP_CLI::line( 'Dry run ā memberships will not be created. Use --live flag to run in live mode.' );
+ }
+ WP_CLI::line( '' );
+
+ global $wpdb;
+ $user_emails_with_ids = $wpdb->get_results( 'SELECT ID, user_email FROM wp_users WHERE user_email != ""' ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ // Turn it into an associative array, keyed by email.
+ $user_emails_with_ids = array_combine(
+ array_map(
+ function( $user ) {
+ return $user->user_email;
+ },
+ $user_emails_with_ids
+ ),
+ array_map(
+ function( $user ) {
+ return $user->ID;
+ },
+ $user_emails_with_ids
+ )
+ );
+ $user_emails = array_keys( $user_emails_with_ids );
+
+ WP_CLI::line( 'Found ' . count( $user_emails ) . ' users.' );
+ WP_CLI::line( '' );
+
+ // Get the registration-based memberships.
+ $plans = \wc_memberships_get_membership_plans();
+ foreach ( $plans as $plan ) {
+ if ( $plan->get_access_method() === 'signup' ) {
+ $memberships = $plan->get_memberships();
+
+ WP_CLI::line( 'Found ' . count( $memberships ) . ' memberships for plan ' . $plan->get_name() . '.' );
+
+ // Get email addresses of all members.
+ foreach ( $memberships as $membership ) {
+ $user_id = $membership->get_user_id();
+ // Get user email directly from the DB for performance reasons.
+ $user_email = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ $wpdb->prepare(
+ 'SELECT user_email FROM wp_users WHERE ID = %d',
+ $user_id
+ )
+ );
+ if ( $user_email ) {
+ // Remove this user from the list of all users.
+ $key = array_search( $user_email, $user_emails );
+ if ( $key !== false ) {
+ unset( $user_emails[ $key ] );
+ }
+ }
+ }
+
+ if ( count( $user_emails ) > 0 ) {
+ if ( $live ) {
+ WP_CLI::line( 'Will create memberships for the ' . count( $user_emails ) . ' missing users.' );
+ } else {
+ WP_CLI::line( 'Would create memberships for the ' . count( $user_emails ) . ' missing users in live mode.' );
+ }
+ WP_CLI::line( '' );
+ foreach ( $user_emails as $email ) {
+ // Create the membership for the user.
+ $user_id = $user_emails_with_ids[ $email ];
+ if ( $verbose ) {
+ if ( $live ) {
+ WP_CLI::line( "Creating membership for user $email (#$user_id)." );
+ } else {
+ WP_CLI::line( "Would create membership for user $email (#$user_id)." );
+ }
+ }
+ if ( $live ) {
+ wc_memberships_create_user_membership(
+ [
+ 'plan_id' => $plan->get_id(),
+ 'user_id' => $user_id,
+ ]
+ );
+ }
+ }
+ }
+ }
+ }
+
+ WP_CLI::line( '' );
+ }
+
+ /**
+ * Fix membership discrepancies.
+ *
+ * @param array $args Indexed array of args.
+ * @param array $assoc_args Associative array of args.
+ * @return void
+ *
+ * ## OPTIONS
+ *
+ * [--live]
+ * : Run the command in live mode, updating the users.
+ *
+ * ## EXAMPLES
+ *
+ * wp newspack-network fix-membership-discrepancies
+ */
+ public static function fix_membership_discrepancies( array $args, array $assoc_args ) {
+ WP_CLI::line( '' );
+
+ $live = isset( $assoc_args['live'] ) ? true : false;
+ if ( $live ) {
+ WP_CLI::line( 'Live mode ā memberships will be updated or created.' );
+ } else {
+ WP_CLI::line( 'Dry run ā memberships will not be updated. Use --live flag to run in live mode.' );
+ }
+
+ $membership_plans_from_network_data = \Newspack_Network\Hub\Admin\Membership_Plans::get_membership_plans_from_network();
+ if ( ! isset( $membership_plans_from_network_data['discrepancies_emails'] ) ) {
+ WP_CLI::error( 'Missing discrepancies emails in network memberships data.' );
+ }
+ WP_CLI::line( sprintf( 'Found %d discrepant email(s).', count( $membership_plans_from_network_data['discrepancies_emails'] ) ) );
+ $by_origin_site = [];
+ foreach ( $membership_plans_from_network_data['discrepancies_emails'] as $email_address ) {
+ $user = get_user_by( 'email', $email_address );
+ if ( ! $user ) {
+ WP_CLI::warning( 'User not found by email: ' . $email_address );
+ continue;
+ }
+ $origin_site = get_user_meta( $user->ID, \Newspack_Network\Utils\Users::USER_META_REMOTE_SITE, true );
+ if ( ! $origin_site ) {
+ $origin_site = 'hub';
+ }
+ if ( isset( $by_origin_site[ $origin_site ] ) ) {
+ $by_origin_site[ $origin_site ][] = $email_address;
+ } else {
+ $by_origin_site[ $origin_site ] = [ $email_address ];
+ }
+ }
+
+ WP_CLI::line( '' );
+ foreach ( $by_origin_site as $site_url => $user_emails ) {
+ WP_CLI::line( sprintf( 'Found %d user(s) originating from site %s', count( $user_emails ), $site_url ) );
+
+ foreach ( $user_emails as $user_email ) {
+ WP_CLI::line( sprintf( 'Fixing %sā¦', $user_email ) );
+
+ if ( $site_url === 'hub' ) {
+ $found_memberships = \Newspack_Network\Woocommerce_Memberships\Admin::get_memberships_by_user_email_with_plan_network_id( $user_email );
+ } else {
+ $found_memberships = \Newspack_Network\Hub\Admin\Membership_Plans::fetch_collection_from_api(
+ \Newspack_Network\Hub\Nodes::get_node_by_url( $site_url ),
+ 'wc/v2/memberships/members',
+ 'memberships',
+ [
+ 'customer' => urlencode( $user_email ),
+ ]
+ );
+ }
+
+ if ( ! $found_memberships ) {
+ WP_CLI::warning( 'Could not retrieve any user memberships.' );
+ continue;
+ }
+ foreach ( $found_memberships as $membership ) {
+ if ( ! $membership->plan_network_id ) {
+ continue;
+ }
+ WP_CLI::line( sprintf( 'Found membership #%d with status %s.', $membership->id, $membership->status ) );
+ $membership_data = [
+ 'email' => $user_email,
+ 'user_id' => $membership->customer_id,
+ 'plan_network_id' => $membership->plan_network_id,
+ 'membership_id' => $membership->id,
+ 'new_status' => $membership->status,
+ ];
+ $timestamp = false;
+ switch ( $membership->status ) {
+ case 'paused':
+ $timestamp = strtotime( $membership->paused_date );
+ break;
+ case 'cancelled':
+ $timestamp = strtotime( $membership->cancelled_date );
+ break;
+ case 'expired':
+ $timestamp = strtotime( $membership->end_date );
+ break;
+ }
+
+ if ( ! $timestamp ) {
+ $timestamp = strtotime( $membership->start_date );
+ }
+
+ $event = new \Newspack_Network\Incoming_Events\Woocommerce_Membership_Updated( $site_url, $membership_data, $timestamp );
+
+ if ( $live ) {
+ if ( Site_Role::is_hub() ) {
+ $event->process_in_hub();
+ if ( $event->is_persisted ) {
+ WP_CLI::success( 'Created and processed event.' );
+ } else {
+ WP_CLI::warning( 'Event not persisted, possibly a duplicate.' );
+ }
+ Data_Backfill::increment_results_counter( $event->get_action_name(), $event->is_persisted ? 'processed' : 'duplicate' );
+ } else {
+ \Newspack\Data_Events\Webhooks::handle_dispatch( $event->get_action_name(), $event->get_timestamp(), $event->get_data() );
+ WP_CLI::success( 'Dispatched webhook.' );
+ }
+ } else {
+ WP_CLI::line( 'Would create and process the event in live mode.' );
+ }
+ }
+ }
+ WP_CLI::line( '' );
+ }
+
+ WP_CLI::line( '' );
+ }
+
+ /**
+ * Display user discrepancies.
+ *
+ * @param array $args Indexed array of args.
+ * @param array $assoc_args Associative array of args.
+ * @return void
+ *
+ * ## EXAMPLES
+ *
+ * wp newspack-network display-user-discrepancies
+ */
+ public static function display_user_discrepancies( array $args, array $assoc_args ) {
+ WP_CLI::line( '' );
+
+ if ( ! Site_Role::is_hub() ) {
+ WP_CLI::error( 'This command can only be run on the Hub.' );
+ }
+
+ $user_emails_on_hub = \Newspack_Network\Utils\Users::get_synchronized_users_emails();
+ WP_CLI::line( sprintf( 'Found %d synchronizable user(s) on the Hub.', count( $user_emails_on_hub ) ) );
+ $user_not_synced_emails_on_hub = \Newspack_Network\Utils\Users::get_not_synchronized_users_emails();
+ WP_CLI::line( sprintf( 'Found %d synchronizable user(s) on the Hub.', count( $user_not_synced_emails_on_hub ) ) );
+ $no_role_users_emails = \Newspack_Network\Utils\Users::get_no_role_users_emails();
+ WP_CLI::line( sprintf( 'Found %d no-role user(s) on the Hub.', count( $no_role_users_emails ) ) );
+
+ WP_CLI::line( '' );
+
+ $nodes = \Newspack_Network\Hub\Nodes::get_all_nodes();
+ $users_by_site = [
+ 'hub' => [
+ 'not_synced' => $user_not_synced_emails_on_hub,
+ 'no_role' => $no_role_users_emails,
+ 'synced' => $user_emails_on_hub,
+ ],
+ ];
+ $all_discrepant_emails = [];
+ foreach ( $nodes as $node ) {
+ $url = $node->get_url();
+
+ $site_info = $node->get_site_info();
+ if ( $site_info === null ) {
+ WP_CLI::warning( 'Missing user data for site ' . $url );
+ WP_CLI::line( '' );
+ continue;
+ }
+
+ $users_by_site[ $url ] = [
+ 'not_synced' => $site_info->not_sync_users_emails,
+ 'no_role' => $site_info->no_role_users_emails,
+ 'synced' => $site_info->sync_users_emails,
+ ];
+
+ // Users who are on the Hub but not on the Node.
+ $not_on_node = array_diff( $user_emails_on_hub, $site_info->sync_users_emails );
+ // Users who are not on the Node but are on the Hub.
+ $not_on_hub = array_diff( $site_info->sync_users_emails, $user_emails_on_hub );
+ WP_CLI::line( 'Site: ' . $url );
+ WP_CLI::line(
+ sprintf( '%1$d on the Hub only, %2$d on Node only', count( $not_on_hub ), count( $not_on_node ) )
+ );
+ WP_CLI::line( sprintf( 'Found %d no-role user(s).', count( $site_info->no_role_users_emails ) ) );
+
+ $unique_synced_email_addresses = array_unique( $site_info->sync_users_emails );
+ WP_CLI::line( sprintf( 'Found %d synced email addresses (%d unique).', count( $site_info->sync_users_emails ), count( $unique_synced_email_addresses ) ) );
+ if ( in_array( '', $site_info->sync_users_emails ) ) {
+ WP_CLI::warning( 'Empty email address found.' );
+ }
+ if ( count( $unique_synced_email_addresses ) !== count( $site_info->sync_users_emails ) ) {
+ $counts = array_count_values( $site_info->sync_users_emails );
+ foreach ( $counts as $email => $count ) {
+ if ( $count > 1 ) {
+ WP_CLI::warning( 'Duplicate email address: ' . $email );
+ }
+ }
+ }
+
+ $all_discrepant_emails = array_unique( array_merge( $all_discrepant_emails, $not_on_node, $not_on_hub ) );
+
+ WP_CLI::line( '' );
+ }
+
+ WP_CLI::line( 'Unique discrepant emails in total: ' . count( $all_discrepant_emails ) );
+ WP_CLI::line( '' );
+
+ $users_to_handle = $all_discrepant_emails;
+
+ // For each user, check who they are on all sites.
+ foreach ( $users_to_handle as $email_address ) {
+ if ( ! $email_address ) {
+ continue;
+ }
+ $user_record = [];
+ foreach ( $users_by_site as $site_url => $email_groups ) {
+ foreach ( $email_groups as $key => $users ) {
+ if ( in_array( $email_address, $users ) ) {
+ $user_record[ $site_url ] = $key;
+ }
+ }
+ }
+ $user_sites = array_keys( $user_record );
+ $all_states = array_unique( array_values( $user_record ) );
+ $wp_user = get_user_by( 'email', $email_address );
+ $user_identification = $email_address;
+ if ( $wp_user ) {
+ $origin_site = get_user_meta( $wp_user->ID, \Newspack_Network\Utils\Users::USER_META_REMOTE_SITE, true );
+ if ( $origin_site ) {
+ $user_identification = sprintf( '%s (%s)', $email_address, $origin_site );
+ }
+ }
+
+ if ( empty( $user_record ) ) {
+ WP_CLI::warning( sprintf( 'User %s is not present on any site.', $user_identification ) );
+ WP_CLI::line( '' );
+ } elseif ( ! in_array( 'synced', $all_states ) ) {
+ WP_CLI::line( sprintf( 'User %s is not synced on any site, they won\'t cause discrepancies.', $user_identification ) );
+ WP_CLI::line( '' );
+ } elseif ( count( $user_sites ) === 1 ) {
+ WP_CLI::success( sprintf( 'User %s is synced on a single site only (%s).', $user_identification, $user_sites[0] ) );
+ WP_CLI::line( '' );
+ } else {
+ if ( count( $user_sites ) === 8 ) {
+ WP_CLI::success( sprintf( 'User %s is synced on all sites.', $user_identification ) );
+ } else {
+ WP_CLI::success( sprintf( 'User %s is present on the following sites:', $user_identification ) );
+ }
+ foreach ( $user_record as $url => $key ) {
+ WP_CLI::line( sprintf( '%s on site %s', $key, $url ) );
+ }
+ WP_CLI::line( '' );
+ }
+ }
+
+ WP_CLI::line( '' );
+ }
+
+
+ /**
+ * Deduplicate users on a site.
+ *
+ * @param array $args Indexed array of args.
+ * @param array $assoc_args Associative array of args.
+ * @return void
+ *
+ * ## OPTIONS
+ *
+ * [--live]
+ * : Run the command in live mode, updating the users.
+ *
+ * ## EXAMPLES
+ *
+ * wp newspack-network deduplicate-users
+ */
+ public static function deduplicate_users( array $args, array $assoc_args ) {
+ WP_CLI::line( '' );
+
+ $live = isset( $assoc_args['live'] ) ? true : false;
+ if ( $live ) {
+ WP_CLI::line( 'Live mode ā users will be deleted.' );
+ } else {
+ WP_CLI::line( 'Dry run ā users will not be deleted. Use --live flag to run in live mode.' );
+ }
+
+ global $wpdb;
+ $duplicate_users_result = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ 'SELECT user_email, ID, COUNT(user_email) as count FROM wp_users GROUP BY user_email HAVING count > 1'
+ );
+ WP_CLI::line( sprintf( 'Found %d duplicated user(s)', count( $duplicate_users_result ) ) );
+ WP_CLI::line( '' );
+ foreach ( $duplicate_users_result as $key => $result ) {
+ if ( empty( $result->user_email ) ) {
+ WP_CLI::warning( 'No email address for user #' . $result->ID );
+ WP_CLI::line( '' );
+ continue;
+ }
+ WP_CLI::line( 'Email address: ' . $result->user_email );
+ $user_ids_results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ $wpdb->prepare(
+ 'SELECT id FROM wp_users WHERE user_email = %s',
+ $result->user_email
+ )
+ );
+ $users = [];
+ foreach ( $user_ids_results as $key => $result ) {
+ $users[] = get_user_by( 'id', $result->id );
+ }
+ $by_user = [];
+ foreach ( $users as $user ) {
+ $memberships = \wc_memberships_get_user_memberships( $user );
+ $by_user[ $user->ID ] = [
+ 'memberships' => $memberships,
+ 'user' => $user,
+ ];
+ if ( count( $memberships ) === 0 ) {
+ WP_CLI::line( sprintf( 'User %d has no memberships', $user->ID ) );
+ } else {
+ $statuses = array_map(
+ function( $membership ) {
+ return $membership->status;
+ },
+ $memberships
+ );
+ $plan_ids = array_map(
+ function( $membership ) {
+ return $membership->plan_id;
+ },
+ $memberships
+ );
+ WP_CLI::line( sprintf( 'User %d has %d membership(s) (plans: %s) (statuses: %s)', $user->ID, count( $memberships ), implode( ', ', $plan_ids ), implode( ', ', $statuses ) ) );
+ }
+ }
+
+ // Get the user with the least memberships.
+ $min_memberships = PHP_INT_MAX;
+ $min_user_id = null;
+ foreach ( $by_user as $user_id => $user_data ) {
+ $memberships = $user_data['memberships'];
+ if ( count( $memberships ) < $min_memberships ) {
+ $min_memberships = count( $memberships );
+ $min_user_id = $user_id;
+ }
+ }
+ if ( $live ) {
+ WP_CLI::line( 'Removing user #' . $min_user_id );
+ // If removing duplicates, the user-deleted event should not be handled. Otherwise,
+ // the "original" user would be removed from the network.
+ add_filter( 'newspack_network_process_user_deleted', '__return_false' );
+ $result = wp_delete_user( $min_user_id );
+ remove_filter( 'newspack_network_process_user_deleted', '__return_false' );
+ if ( $result ) {
+ WP_CLI::success( 'Deleted user ' . $min_user_id );
+ } else {
+ WP_CLI::warning( 'Failed to delete user ' . $min_user_id );
+ }
+ } else {
+ WP_CLI::line( 'Would remove user #' . $min_user_id );
+ }
+
+ WP_CLI::line( '' );
+ }
+ }
+
+ /**
+ * Deduplicate subscriptions.
+ *
+ * @param array $args Indexed array of args.
+ * @param array $assoc_args Associative array of args.
+ * @return void
+ *
+ * ## OPTIONS
+ *
+ * [--live]
+ * : Run the command in live mode, updating the subscriptions.
+ *
+ * [--fix-memberships]
+ * : Fix mismatch between active subscriptions and active memberships.
+ *
+ * ## EXAMPLES
+ *
+ * wp newspack-network fix-subscriptions
+ */
+ public static function fix_subscriptions( array $args, array $assoc_args ) {
+ WP_CLI::line( '' );
+
+ $live = isset( $assoc_args['live'] ) ? true : false;
+ $fix_memberships = isset( $assoc_args['fix-memberships'] ) ? true : false;
+
+ if ( $live ) {
+ WP_CLI::line( 'Live mode ā data will be updated.' );
+ } else {
+ WP_CLI::line( 'Dry run ā data will not be updated. Use --live flag to run in live mode.' );
+ }
+ WP_CLI::line( '' );
+
+ global $wpdb;
+ if ( $fix_memberships ) {
+ // Query for all subscriptions, regardless of status.
+ $subscriptions_per_user_results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ "SELECT customer_id, COUNT(customer_id) as count FROM wp_wc_orders WHERE type = 'shop_subscription' GROUP BY customer_id HAVING count > 1"
+ );
+ WP_CLI::line( sprintf( 'Found %d user(s) with multiple subscriptions', count( $subscriptions_per_user_results ) ) );
+ } else {
+ // Query for active subscriptions only.
+ $subscriptions_per_user_results = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
+ "SELECT customer_id, COUNT(customer_id) as count FROM wp_wc_orders WHERE type = 'shop_subscription' AND status = 'wc-active' GROUP BY customer_id HAVING count > 1"
+ );
+ WP_CLI::line( sprintf( 'Found %d user(s) with multiple active subscriptions', count( $subscriptions_per_user_results ) ) );
+ }
+ WP_CLI::line( '' );
+
+ foreach ( $subscriptions_per_user_results as $result ) {
+ $user = get_user_by( 'id', $result->customer_id );
+ if ( $user ) {
+ // Compare to user's memberships.
+ $active_memberships = \wc_memberships_get_user_memberships( $user->ID, [ 'status' => [ 'active' ] ] );
+ if ( $fix_memberships ) {
+ WP_CLI::line( sprintf( 'User %s (#%d) has %d subscriptions and %d active memberships.', $user->user_email, $result->customer_id, $result->count, count( $active_memberships ) ) );
+ } else {
+ WP_CLI::line( sprintf( 'User %s (#%d) has %d active subscriptions and %d active memberships.', $user->user_email, $result->customer_id, $result->count, count( $active_memberships ) ) );
+ }
+ $memberships_subscriptions_delta = $result->count - count( $active_memberships );
+ if ( $memberships_subscriptions_delta > 0 ) {
+ if ( $fix_memberships ) {
+ $active_subscription_ids = [];
+ $subscription_amounts = [];
+ $user_subscriptions = \wcs_get_users_subscriptions( $user->ID );
+ foreach ( $user_subscriptions as $subscription ) {
+ WP_CLI::line( sprintf( 'Subscription #%d (amount: $%d, started %s) has status "%s"', $subscription->get_id(), $subscription->get_total(), $subscription->get_date( 'start' ), $subscription->get_status() ) );
+ $subscription_amounts[] = $subscription->get_total();
+ if ( $subscription->get_status() === 'active' ) {
+ $active_subscription_ids[] = $subscription->get_id();
+ }
+ }
+ // If all subscription amounts are the same, this might be a mistake.
+ if ( count( $user_subscriptions ) !== count( array_unique( $subscription_amounts ) ) ) {
+ WP_CLI::warning( 'Found subscriptions with the same amount, this might be a mistake!' );
+ }
+
+ if ( empty( $active_subscription_ids ) && ! empty( $active_memberships ) ) {
+ WP_CLI::warning( 'No active subscriptions, but has active membership(s)!' );
+ }
+
+ if ( empty( $active_memberships ) ) {
+ WP_CLI::line( sprintf( 'User %s (#%d) has %d subscriptions and no active memberships.', $user->user_email, $result->customer_id, $result->count ) );
+ WP_CLI::line( '' );
+ continue;
+ }
+
+ foreach ( $active_memberships as $membership ) {
+ $membership_subscription = $membership->get_subscription();
+ WP_CLI::line( sprintf( 'Membership #%d is tied to subscription #%d', $membership->get_id(), $membership_subscription ? $membership_subscription->get_id() : 0 ) );
+ if ( $membership_subscription && ! in_array( $membership_subscription->get_id(), $active_subscription_ids ) ) {
+ if ( empty( $active_subscription_ids ) ) {
+ WP_CLI::warning( 'No active subscriptions, cannot fix the membership!' );
+ continue;
+ }
+ if ( $live ) {
+ $membership->set_subscription_id( $active_subscription_ids[0] );
+ $membership->set_end_date(); // CLear end date so membership is tied to subscription.
+ $membership->update_status( 'wcm-active' );
+ WP_CLI::success( sprintf( 'The subscription (%d) tied to the active membership is not an active subscription. It has been linked to an active subscription (%d).', $membership_subscription->get_id(), $active_subscription_ids[0] ) );
+ } else {
+ WP_CLI::warning( sprintf( 'The subscription (%d) tied to the active membership is not an active subscription.', $membership_subscription->get_id() ) );
+ }
+ }
+ }
+ }
+ }
+ } else {
+ WP_CLI::line( sprintf( '%d active subscriptions are assigned to user #%d (no user found)', $result->count, $result->customer_id ) );
+ }
+ WP_CLI::line( '' );
+ }
+
+ WP_CLI::line( '' );
+ }
+
+ /**
+ * Reprocess specified events from the event log.
+ *
+ * @param array $args Indexed array of args.
+ * @param array $assoc_args Associative array of args.
+ * @return void
+ *
+ * ## OPTIONS
+ *
+ * [--live]
+ * : Run the command in live mode, updating the subscriptions.
+ *
+ * ## EXAMPLES
+ *
+ * wp newspack-network reprocess-events 123 42
+ */
+ public static function reprocess_events( array $args, array $assoc_args ) {
+ WP_CLI::line( '' );
+ if ( ! Site_Role::is_hub() ) {
+ WP_CLI::error( 'This command can only be run on the Hub site.' );
+ }
+ $event_ids = $args;
+ if ( empty( $event_ids ) ) {
+ WP_CLI::error( 'Please provide event IDs to reprocess.' );
+ }
+
+ $live = isset( $assoc_args['live'] ) ? true : false;
+
+ global $wpdb;
+ $table_name = \Newspack_Network\Hub\Database\Event_Log::get_table_name();
+ $query = "SELECT * FROM $table_name WHERE id IN (" . implode( ',', $event_ids ) . ')';
+ $results = $wpdb->get_results( $query ); //phpcs:ignore
+ if ( empty( $results ) ) {
+ WP_CLI::error( 'No events found with the provided IDs.' );
+ }
+ foreach ( $results as $event ) {
+ $incoming_event_class = 'Newspack_Network\\Incoming_Events\\' . Accepted_Actions::ACTIONS[ $event->action_name ];
+ $incoming_event = new $incoming_event_class( false, json_decode( $event->data ), $event->timestamp );
+ if ( $live ) {
+ WP_CLI::line( sprintf( 'Reprocessing event #%d of action: %s.', $event->id, $event->action_name ) );
+ $incoming_event->post_process_in_hub();
+ } else {
+ WP_CLI::line( sprintf( 'Would reprocess event #%d of action: %s.', $event->id, $event->action_name ) );
+ }
+ }
+ WP_CLI::line( '' );
+ }
+}
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 '