diff --git a/includes/admin/class-users-columns.php b/includes/admin/class-users-columns.php index 81165829..5ad62275 100644 --- a/includes/admin/class-users-columns.php +++ b/includes/admin/class-users-columns.php @@ -350,9 +350,6 @@ if ( ! class_exists( 'um\admin\Users_Columns' ) ) { return; } - // Set default statuses if not already done. - UM()->setup()->set_default_user_status(); - $id = 'um_user_status'; // need to add there additional nonce field because WordPress native _wpnonce field isn't visible on the users.php screen then custom actions diff --git a/includes/admin/core/class-admin-notices.php b/includes/admin/core/class-admin-notices.php index 0fbc12ab..be73e0f4 100644 --- a/includes/admin/core/class-admin-notices.php +++ b/includes/admin/core/class-admin-notices.php @@ -47,6 +47,8 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { $this->lock_registration(); + $this->empty_status_users(); + $this->extensions_page(); $this->child_theme_required(); @@ -204,17 +206,17 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { ob_start(); ?> -
' . wp_kses( sprintf( __( 'Background process is running: Setting user statuses %1$d/%2$d.', 'ultimate-member' ), $empty_status_users[0], $empty_status_users[1] ), $allowed_html ) . '
', + 'dismissible' => false, + ) + ); + } + /** * Checking if the "Membership - Anyone can register" WordPress general setting is active */ @@ -256,11 +289,11 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { ?>- All Access Pass – Get access to all Ultimate Member extensions at a significant discount with our All Access Pass.', 'ultimate-member' ) ?> + All Access Pass – Get access to all Ultimate Member extensions at a significant discount with our All Access Pass.', 'ultimate-member' ); ?>
@@ -290,7 +323,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { $path = str_replace( '//', '/', $path ); if ( ! file_exists( $path ) ) { - $old = umask(0); + $old = umask( 0 ); @mkdir( $path, 0777, true ); umask( $old ); } @@ -326,9 +359,12 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { 'woocommerce', ); - $slugs = array_map( function( $item ) { - return 'um-' . $item . '/um-' . $item . '.php'; - }, $old_extensions ); + $slugs = array_map( + function ( $item ) { + return 'um-' . $item . '/um-' . $item . '.php'; + }, + $old_extensions + ); $active_plugins = UM()->dependencies()->get_active_plugins(); foreach ( $slugs as $slug ) { @@ -622,7 +658,7 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { $arr_inactive_license_keys[] = $license->item_name; } - $invalid_license++; + ++$invalid_license; } if ( ! empty( $arr_inactive_license_keys ) ) { @@ -676,34 +712,41 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { - add_notice( 'upgrade', array( - 'class' => 'error', - 'message' => $message, - ), 4 ); - } else { - if ( isset( $_GET['msg'] ) && 'updated' === sanitize_key( $_GET['msg'] ) ) { - if ( isset( $_GET['page'] ) && 'um_options' === sanitize_key( $_GET['page'] ) ) { - $this->add_notice( 'settings_upgrade', array( - 'class' => 'updated', - 'message' => '' . __( 'Settings successfully upgraded', 'ultimate-member' ) . '
', - ), 4 ); - } else { - $this->add_notice( - 'upgrade', - array( - 'class' => 'updated', - // translators: %1$s is a plugin name title; %2$s is a plugin version. - 'message' => '' . sprintf( __( '%1$s %2$s Successfully Upgraded', 'ultimate-member' ), UM_PLUGIN_NAME, UM_VERSION ) . '
', - ), - 4 - ); - } + $this->add_notice( + 'upgrade', + array( + 'class' => 'error', + 'message' => $message, + ), + 4 + ); + } elseif ( isset( $_GET['msg'] ) && 'updated' === sanitize_key( $_GET['msg'] ) ) { + if ( isset( $_GET['page'] ) && 'um_options' === sanitize_key( $_GET['page'] ) ) { + $this->add_notice( + 'settings_upgrade', + array( + 'class' => 'updated', + 'message' => '' . __( 'Settings successfully upgraded', 'ultimate-member' ) . '
', + ), + 4 + ); + } else { + $this->add_notice( + 'upgrade', + array( + 'class' => 'updated', + // translators: %1$s is a plugin name title; %2$s is a plugin version. + 'message' => '' . sprintf( __( '%1$s %2$s Successfully Upgraded', 'ultimate-member' ), UM_PLUGIN_NAME, UM_VERSION ) . '
', + ), + 4 + ); } } } @@ -734,18 +777,18 @@ if ( ! class_exists( 'um\admin\core\Admin_Notices' ) ) { ?>- add_notice( 'future_changes', array( - 'class' => 'updated', - 'message' => $message, - ), 2 ); + $this->add_notice( + 'future_changes', + array( + 'class' => 'updated', + 'message' => $message, + ), + 2 + ); } /** diff --git a/includes/common/actions/class-users.php b/includes/common/actions/class-users.php new file mode 100644 index 00000000..e3309b6a --- /dev/null +++ b/includes/common/actions/class-users.php @@ -0,0 +1,142 @@ +maybe_action_scheduler()->next_scheduled_action( self::SCHEDULE_ACTION ) ) { + return; + } + + UM()->maybe_action_scheduler()->schedule_recurring_action( + time() + 60, + self::INTERVAL, + self::SCHEDULE_ACTION + ); + } + + public function status_check() { + $total_users = UM()->common()->users()::get_empty_status_users(); + if ( empty( $total_users ) ) { + return; + } + + UM()->maybe_action_scheduler()->enqueue_async_action( + self::BATCH_ACTION, + array( + 'page' => 1, + 'total' => $total_users, + 'pages' => ceil( $total_users / self::BATCH_SIZE ), + ) + ); + } + + /** + * Perform batch checking for users based on specific conditions. + * Ignore users with `_um_registration_in_progress` that can be in the process of the registration. + * Get users with empty `account_status` meta. + * + * @param int $page The current page number. + * @param int $total The total number of users to process. + * @param int $pages The total number of pages to process. + */ + public function batch_check( $page, $total, $pages ) { + $users = new WP_User_Query( + array( + 'number' => self::BATCH_SIZE, + 'fields' => 'ids', + 'meta_query' => array( + 'relation' => 'AND', + array( + 'relation' => 'OR', + array( + 'key' => '_um_registration_in_progress', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => '_um_registration_in_progress', + 'value' => '1', + 'compare' => '!=', + ), + ), + array( + 'relation' => 'OR', + array( + 'key' => 'account_status', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => 'account_status', + 'value' => '', + 'compare' => '=', + ), + ), + ), + ) + ); + + $results = $users->get_results(); + + if ( ! empty( $results ) ) { + $um_empty_status_users = get_option( '_um_log_empty_status_users', array( 0, 0 ) ); + if ( ! is_array( $um_empty_status_users ) ) { + $um_empty_status_users = array( 0, count( $results ) ); + } + + foreach ( $results as $user_id ) { + $res = UM()->common()->users()->approve( $user_id, true, true ); + if ( $res ) { + ++$um_empty_status_users[0]; + } + } + + if ( $um_empty_status_users[0] < $um_empty_status_users[1] ) { + update_option( '_um_log_empty_status_users', $um_empty_status_users ); + } else { + delete_option( '_um_log_empty_status_users' ); + } + + $next_page = $page + 1; + if ( $next_page <= $pages ) { + UM()->maybe_action_scheduler()->enqueue_async_action( + self::BATCH_ACTION, + array( + 'page' => $next_page, + 'total' => $total, + 'pages' => $pages, + ) + ); + } + } + } + } +} diff --git a/includes/common/class-init.php b/includes/common/class-init.php index 73cf4805..2ceff24f 100644 --- a/includes/common/class-init.php +++ b/includes/common/class-init.php @@ -39,6 +39,10 @@ if ( ! class_exists( 'um\common\Init' ) ) { if ( empty( UM()->classes['um\common\actions\emails'] ) ) { UM()->classes['um\common\actions\emails'] = new actions\Emails(); } + + if ( empty( UM()->classes['um\common\actions\users'] ) ) { + UM()->classes['um\common\actions\users'] = new actions\Users(); + } // Other classes init here as soon as possible. } diff --git a/includes/common/class-users.php b/includes/common/class-users.php index e6fcaa5e..392c29d2 100644 --- a/includes/common/class-users.php +++ b/includes/common/class-users.php @@ -658,11 +658,12 @@ class Users { * * @param int $user_id User ID. * @param bool $force If true - ignore current user condition. + * @param bool $silent If true - don't send email notification. E.g. case when user already exists, but doesn't have a status. * * @return bool `true` if the user has been approved * `false` on failure or if the user already has approved status. */ - public function approve( $user_id, $force = false ) { + public function approve( $user_id, $force = false, $silent = false ) { if ( ! $this->can_be_approved( $user_id, $force ) ) { return false; } @@ -683,25 +684,27 @@ class Users { // It's `false` on failure or if the user already has approved status. if ( false !== $result ) { - $userdata = get_userdata( $user_id ); + if ( false === $silent ) { + $userdata = get_userdata( $user_id ); - $this->reset_activation_link( $user_id ); + $this->reset_activation_link( $user_id ); - $email_slug = 'welcome_email'; - if ( 'awaiting_admin_review' === $old_status ) { - $email_slug = 'approved_email'; - $this->maybe_generate_password_reset_key( $userdata ); + $email_slug = 'welcome_email'; + if ( 'awaiting_admin_review' === $old_status ) { + $email_slug = 'approved_email'; + $this->maybe_generate_password_reset_key( $userdata ); + } + + $current_user_id = get_current_user_id(); + um_fetch_user( $user_id ); + + add_filter( 'um_template_tags_patterns_hook', array( UM()->password(), 'add_placeholder' ) ); + add_filter( 'um_template_tags_replaces_hook', array( UM()->password(), 'add_replace_placeholder' ) ); + + UM()->maybe_action_scheduler()->enqueue_async_action( 'um_dispatch_email', array( $userdata->user_email, $email_slug, array( 'fetch_user_id' => $user_id ) ) ); + + um_fetch_user( $current_user_id ); } - - $current_user_id = get_current_user_id(); - um_fetch_user( $user_id ); - - add_filter( 'um_template_tags_patterns_hook', array( UM()->password(), 'add_placeholder' ) ); - add_filter( 'um_template_tags_replaces_hook', array( UM()->password(), 'add_replace_placeholder' ) ); - - UM()->maybe_action_scheduler()->enqueue_async_action( 'um_dispatch_email', array( $userdata->user_email, $email_slug, array( 'fetch_user_id' => $user_id ) ) ); - - um_fetch_user( $current_user_id ); /** * Fires after User has been approved. * @@ -827,4 +830,29 @@ class Users { $user = WP_Session_Tokens::get_instance( $user_id ); $user->destroy_all(); } + + /** + * Retrieve the number of users with empty `account_status` usermeta. + * + * @return int + */ + public static function get_empty_status_users() { + global $wpdb; + + $total_users = $wpdb->get_var( + "SELECT COUNT(u.ID) + FROM {$wpdb->users} u + LEFT JOIN {$wpdb->usermeta} um ON u.ID = um.user_id AND um.meta_key = 'account_status' + LEFT JOIN {$wpdb->usermeta} um2 ON u.ID = um2.user_id AND um2.meta_key = '_um_registration_in_progress' + WHERE ( um.meta_value IS NULL OR um.meta_value = '' ) AND + um2.meta_value IS NULL OR um2.meta_value != '1'" + ); + + $total_users = absint( $total_users ); + // In WordPress, an underscore prefix before the option name (e.g., _my_option_name) is commonly used to indicate that the option is private. + // This option has a format: {updated_users}/{total_users_for_update}. + update_option( '_um_log_empty_status_users', array( 0, $total_users ) ); + + return $total_users; + } } diff --git a/includes/core/class-setup.php b/includes/core/class-setup.php index 27a342eb..eb6d37f4 100644 --- a/includes/core/class-setup.php +++ b/includes/core/class-setup.php @@ -310,37 +310,21 @@ KEY meta_value_indx (um_value(191)) * @since 2.4.2 */ public function set_default_user_status() { - $result = get_transient( 'um_count_users_unassigned' ); - if ( false === $result ) { - $args = array( - 'fields' => 'ids', - 'number' => 0, - 'meta_query' => array( - array( - 'key' => 'account_status', - 'compare' => 'NOT EXISTS', - ), - ), - 'um_custom_user_query' => true, - ); - - $users = new WP_User_Query( $args ); - if ( empty( $users ) || is_wp_error( $users ) ) { - $result = array(); - } else { - $result = $users->get_results(); - } - - set_transient( 'um_count_users_unassigned', $result, DAY_IN_SECONDS ); - } - - if ( empty( $result ) ) { + $total_users = UM()->common()->users()::get_empty_status_users(); + if ( empty( $total_users ) ) { return; } - foreach ( $result as $user_id ) { - update_user_meta( $user_id, 'account_status', 'approved' ); - } + // If there are some users without `account_status` then run the first async batch for update. + $batch_size = 50; // See the class constant value `\um\common\actions\Users::BATCH_ACTION`. + UM()->maybe_action_scheduler()->enqueue_async_action( + $batch_size, + array( + 'page' => 1, + 'total' => $total_users, + 'pages' => ceil( $total_users / $batch_size ), + ) + ); } /** diff --git a/includes/core/class-user.php b/includes/core/class-user.php index f8d70584..66baf197 100644 --- a/includes/core/class-user.php +++ b/includes/core/class-user.php @@ -640,7 +640,6 @@ if ( ! class_exists( 'um\core\User' ) ) { UM()->files()->remove_dir( UM()->files()->upload_temp ); UM()->files()->remove_dir( UM()->uploader()->get_upload_base_dir() . um_user( 'ID' ) . DIRECTORY_SEPARATOR ); - delete_transient( 'um_count_users_unassigned' ); delete_transient( 'um_count_users_pending_dot' ); } @@ -959,8 +958,6 @@ if ( ! class_exists( 'um\core\User' ) ) { /** This action is documented in ultimate-member/includes/common/um-actions-register.php */ do_action( 'um_user_register', $user_id, $_POST, null ); } - - delete_transient( 'um_count_users_unassigned' ); } diff --git a/includes/core/um-actions-register.php b/includes/core/um-actions-register.php index 29a73e1a..007101e8 100644 --- a/includes/core/um-actions-register.php +++ b/includes/core/um-actions-register.php @@ -179,6 +179,8 @@ function um_check_user_status( $user_id, $args, $form_data = null ) { */ do_action( "um_post_registration_{$registration_status}_hook", $user_id, $args, $form_data ); + delete_user_meta( $user_id, '_um_registration_in_progress' ); // Status is set. We can delete this marker. + if ( is_null( $form_data ) || is_admin() ) { return; } @@ -513,6 +515,11 @@ function um_submit_form_register( $args, $form_data ) { 'user_pass' => $user_password, 'user_email' => $user_email, 'role' => $user_role, + 'meta_input' => array( + // It's used to ignore users who cannot finish the registration process in the scheduled tasks + // to set 'approved' status to the users without `account_status` meta. + '_um_registration_in_progress' => true, + ), ); $user_id = wp_insert_user( $userdata ); diff --git a/tests/generate-empty-status-users.php b/tests/generate-empty-status-users.php new file mode 100644 index 00000000..55e26ae2 --- /dev/null +++ b/tests/generate-empty-status-users.php @@ -0,0 +1,34 @@ + $random_user_name, + 'user_pass' => 'q1q2q1q2', + 'user_email' => $random_user_email, + 'first_name' => $random_first_name, + 'last_name' => $random_last_name, + 'role' => 'subscriber', + ); + + $user_id = wp_insert_user( $userdata ); + + if ( is_wp_error( $user_id ) ) { + // Something went wrong, handle the error + var_dump( 'User creation failed: ' . $user_id->get_error_message() ); + } else { + var_dump( 'User creation complete: ID:' . $user_id . ' Username:' . $userdata['user_login'] ); + } +} + +exit; diff --git a/uninstall.php b/uninstall.php index 6cf0d554..5cf4e8ab 100644 --- a/uninstall.php +++ b/uninstall.php @@ -127,7 +127,7 @@ if ( ! empty( $delete_options ) ) { delete_transient( "um_count_users_{$status}" ); } delete_transient( 'um_count_users_pending_dot' ); - delete_transient( 'um_count_users_unassigned' ); + delete_transient( 'um_count_users_unassigned' ); // legacy but still need to delete while uninstall. //remove all users cache UM()->user()->remove_cache_all_users();