diff --git a/plugins/bcc-login/includes/class-bcc-login-cleanup.php b/plugins/bcc-login/includes/class-bcc-login-cleanup.php index 54dfe18..e876899 100644 --- a/plugins/bcc-login/includes/class-bcc-login-cleanup.php +++ b/plugins/bcc-login/includes/class-bcc-login-cleanup.php @@ -1,13 +1,16 @@ get_col( + "SELECT DISTINCT m.user_id + FROM {$wpdb->usermeta} m + LEFT JOIN {$wpdb->users} u ON u.ID = m.user_id + WHERE u.ID IS NULL AND m.user_id > 0" + ); + + if ( $wpdb->last_error ) { + return false; + } + + $user_ids = array_map( 'intval', $user_ids ); + + if ( $only_missing_before ) { + $before = get_option( self::MISSING_USERS_OPTION ); + $now = array( 'time' => time(), 'user_ids' => $user_ids ); + update_option( self::MISSING_USERS_OPTION, $now, false ); + + // Only continue once this list is saved, so the next run doesn't compare against an older one. + // update_option() also returns false when the value didn't change, so read it back instead. + if ( get_option( self::MISSING_USERS_OPTION ) !== $now ) { + return false; + } + + $missing_before = array(); + if ( isset( $before['time'], $before['user_ids'] ) && $before['time'] <= time() - 12 * HOUR_IN_SECONDS ) { + $missing_before = array_map( 'intval', (array) $before['user_ids'] ); + } + + $user_ids = array_intersect( $user_ids, $missing_before ); + } + + $deleted = 0; + foreach ( array_chunk( $user_ids, 500 ) as $chunk ) { + // Unlike one DELETE joining the whole table, this only locks these users' meta rows (and briefly the index + // gaps next to them and in wp_users) for one short statement per batch. The join checks again that each + // user is still missing. + $result = $wpdb->query( + "DELETE m + FROM {$wpdb->usermeta} m + LEFT JOIN {$wpdb->users} u ON u.ID = m.user_id + WHERE u.ID IS NULL AND m.user_id IN (" . implode( ',', $chunk ) . ")" + ); + + if ( false === $result ) { + return false; + } + + $deleted += $result; + + self::clean_deleted_users_cache( $chunk ); + } + + return $deleted; + } + + /** + * Removes deleted users from the object cache. With a persistent object cache, they could otherwise + * still be loaded from it. Clearing the 'users' group by id is enough for lookups by login, email or + * slug as well, as those only map to the id and then load the user by it. + * + * @param int[] $user_ids + */ + static function clean_deleted_users_cache( $user_ids ) { + foreach ( $user_ids as $user_id ) { + wp_cache_delete( $user_id, 'users' ); + wp_cache_delete( $user_id, 'user_meta' ); + } + } + /** * Deletes the tokens and claims that the old OpenID Connect Generic plugin stored as user meta. * diff --git a/plugins/bcc-login/includes/class-bcc-login-settings.php b/plugins/bcc-login/includes/class-bcc-login-settings.php index 7baf398..9905483 100644 --- a/plugins/bcc-login/includes/class-bcc-login-settings.php +++ b/plugins/bcc-login/includes/class-bcc-login-settings.php @@ -677,6 +677,13 @@ function delete_subscribers_handler() { ) { global $wpdb; + // Remember who gets deleted, to remove them from the object cache afterwards. The cleanup below only + // finds users that still have meta left, which isn't the case if the role was their only meta. + $subscriber_ids = array_map( 'intval', $wpdb->get_col( + "SELECT user_id FROM {$wpdb->prefix}usermeta + WHERE meta_key = '{$wpdb->prefix}capabilities' AND meta_value LIKE 'a:1:{s:10:\"subscriber\";b:1;}'" + ) ); + $sql = "DELETE {$wpdb->prefix}users, {$wpdb->prefix}usermeta FROM {$wpdb->prefix}users INNER JOIN {$wpdb->prefix}usermeta ON {$wpdb->prefix}users.ID = {$wpdb->prefix}usermeta.user_id @@ -684,23 +691,28 @@ function delete_subscribers_handler() { meta_key = '{$wpdb->prefix}capabilities' AND meta_value LIKE 'a:1:{s:10:\"subscriber\";b:1;}'"; - $wpdb->query($sql); + $subscribers_deleted = $wpdb->query($sql); + + BCC_Login_Cleanup::clean_deleted_users_cache( $subscriber_ids ); + + if ( false === $subscribers_deleted ) { + add_settings_error( + 'subscribers_deleted', + 'subscribers_not_deleted', + __( 'The subscribers could not be deleted.', 'bcc-login' ), + 'error' + ); + } // The join above only removes the capabilities meta row of each subscriber, // so remove all remaining meta (e.g. openid-connect-generic-*) of users that no longer exist. - // The users tables are shared by all sites of a multisite network, so these aren't prefixed per site. - $sql = "DELETE {$wpdb->usermeta} - FROM {$wpdb->usermeta} - LEFT JOIN {$wpdb->users} ON {$wpdb->usermeta}.user_id = {$wpdb->users}.ID - WHERE {$wpdb->users}.ID IS NULL"; - - $orphaned_meta_deleted = $wpdb->query($sql); + $orphaned_meta_deleted = BCC_Login_Cleanup::delete_orphaned_user_meta(); if ( false === $orphaned_meta_deleted ) { add_settings_error( 'subscribers_deleted', 'orphaned_user_meta_not_deleted', - __( 'The user meta of deleted users could not be deleted.', 'bcc-login' ), + __( 'Not all user meta of deleted users could be deleted.', 'bcc-login' ), 'error' ); } @@ -731,8 +743,12 @@ function delete_subscribers_handler() { // but skips the database when an object cache is in use, so force it here. delete_expired_transients( true ); - // Only report the counts of the steps that succeeded, the others have an error notice. - $messages = array( __( 'All subscribers were successfully deleted.' ) ); + // Only report the steps that succeeded, the others have an error notice. + $messages = array(); + + if ( false !== $subscribers_deleted ) { + $messages[] = __( 'All subscribers were successfully deleted.' ); + } if ( false !== $orphaned_meta_deleted ) { $messages[] = sprintf( __( 'Removed %d user meta rows of deleted users.', 'bcc-login' ), $orphaned_meta_deleted );