Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
228 changes: 209 additions & 19 deletions plugins/bcc-login/includes/class-bcc-login-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -657,6 +657,11 @@ function render_delete_subscribers_button () {
<h2><?php _e( 'Maintenance', 'bcc-login' ); ?></h2>
<p><?php _e( 'We recommend deleting subscribers that were automatically generated by earlier versions of the login plugin.', 'bcc-login') ?></p>
<p><?php _e( 'This also removes the tokens and login data stored by earlier versions for all users, and expired transients.', 'bcc-login') ?></p>
<?php if ( is_multisite() && $this->_settings->create_missing_users ) : ?>
<p><?php _e( 'On a network, subscribers who also belong to another site are kept: with OIDC_CREATE_USERS on, they couldn\'t log in here again after only leaving this site. Network administrators are left alone. Only a network administrator can delete user accounts.', 'bcc-login' ) ?></p>
<?php elseif ( is_multisite() ) : ?>
<p><?php _e( 'On a network, subscribers who also belong to another site only leave this site, and network administrators are left alone. Only a network administrator can delete user accounts.', 'bcc-login' ) ?></p>
<?php endif; ?>
<form method="post">
<?php wp_nonce_field( 'delete_subscribers', 'delete_subscribers_nonce' ); ?>
<?php submit_button('Delete subscribers', 'delete', 'delete_subscribers', false, array(
Expand All @@ -680,25 +685,46 @@ 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;}'"
) );
// Users whose only role on this site is subscriber. On a network the users and usermeta tables are
// shared by all sites, and the role is in this site's capabilities meta.
$capabilities_key = $wpdb->get_blog_prefix() . 'capabilities';
$subscriber_ids = array_map( 'intval', $wpdb->get_col( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->usermeta} WHERE meta_key = %s AND meta_value = %s",
$capabilities_key,
'a:1:{s:10:"subscriber";b:1;}'
) ) );

// Of those, the ones who also belong to another site of the network. Null when a query failed.
$member_ids = '' === $wpdb->last_error ? array() : null;
if ( is_multisite() && null !== $member_ids ) {
// Network administrators are left alone, whatever their role on this site.
$subscriber_ids = array_values( array_diff( $subscriber_ids, $this->get_network_admin_ids() ) );
$member_ids = $this->get_other_sites_member_ids( $subscriber_ids, $capabilities_key );
}

$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
WHERE
meta_key = '{$wpdb->prefix}capabilities' AND
meta_value LIKE 'a:1:{s:10:\"subscriber\";b:1;}'";
// On a network, subscribers who also belong to another site keep their account and only leave this
// site, and only network administrators may delete accounts. With OIDC_CREATE_USERS on, the login
// can't add an existing account back to a site, so nobody only leaves: they're kept until they can
// be deleted.
$can_delete = ! is_multisite() || current_user_can( 'delete_users' );
$deleted_ids = array();
$removed_ids = array();
if ( null !== $member_ids ) {
$deleted_ids = $can_delete ? array_values( array_diff( $subscriber_ids, $member_ids ) ) : array();
$removed_ids = $this->_settings->create_missing_users ? array() : array_values( array_diff( $subscriber_ids, $deleted_ids ) );
}
$kept_ids = array_values( array_diff( $subscriber_ids, $deleted_ids, $removed_ids ) );

$subscribers_deleted = $wpdb->query($sql);
// Their logins, email addresses and slugs, to clear them from the object cache afterwards.
$cached_users = $this->get_cache_keys_of_users( array_merge( $deleted_ids, $removed_ids ) );

BCC_Login_Cleanup::clean_deleted_users_cache( $subscriber_ids );
// Accounts first: if that fails, they're still subscribers, so deleting them again finds them.
// Nothing changes when the users couldn't be looked up.
$subscribers_deleted = null !== $member_ids && $this->delete_users( $deleted_ids ) && $this->remove_users_from_site( $removed_ids );

if ( false === $subscribers_deleted ) {
$this->clean_users_cache( $cached_users );

if ( ! $subscribers_deleted ) {
add_settings_error(
'subscribers_deleted',
'subscribers_not_deleted',
Expand All @@ -707,8 +733,8 @@ function delete_subscribers_handler() {
);
}

// 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.
// Deleting an account above leaves its meta, so remove all meta (e.g. openid-connect-generic-*)
// of users that no longer exist.
$orphaned_meta_deleted = BCC_Login_Cleanup::delete_orphaned_user_meta();

if ( false === $orphaned_meta_deleted ) {
Expand Down Expand Up @@ -749,8 +775,44 @@ function delete_subscribers_handler() {
// 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 ( $subscribers_deleted ) {
if ( ! is_multisite() ) {
$messages[] = __( 'All subscribers were successfully deleted.' );
} else {
if ( $can_delete ) {
$messages[] = sprintf( _n( 'Deleted %d subscriber.', 'Deleted %d subscribers.', count( $deleted_ids ), 'bcc-login' ), count( $deleted_ids ) );
}

if ( $removed_ids ) {
$messages[] = $can_delete
? sprintf(
_n(
'%d subscriber belongs to another site of the network, so they were only removed from this site.',
'%d subscribers belong to another site of the network, so they were only removed from this site.',
count( $removed_ids ),
'bcc-login'
),
count( $removed_ids )
)
: sprintf( _n( 'Removed %d subscriber from this site.', 'Removed %d subscribers from this site.', count( $removed_ids ), 'bcc-login' ), count( $removed_ids ) );
}

if ( $kept_ids ) {
$messages[] = sprintf(
_n(
'%d subscriber was kept: with OIDC_CREATE_USERS on, they couldn\'t log in here again after only leaving this site.',
'%d subscribers were kept: with OIDC_CREATE_USERS on, they couldn\'t log in here again after only leaving this site.',
count( $kept_ids ),
'bcc-login'
),
count( $kept_ids )
);
}

if ( ! $can_delete ) {
$messages[] = __( 'Only a network administrator can delete user accounts.', 'bcc-login' );
}
}
}

if ( false !== $orphaned_meta_deleted ) {
Expand All @@ -773,6 +835,134 @@ function delete_subscribers_handler() {
}
}

/**
* The user ids of the network administrators.
*/
private function get_network_admin_ids() {
$ids = array();

foreach ( get_super_admins() as $login ) {
$user = get_user_by( 'login', $login );
if ( $user ) {
$ids[] = $user->ID;
}
}

return $ids;
}

/**
* Of the given users, those who belong to another site of the network: they have its capabilities meta,
* with or without a role. Null when a query failed.
*/
private function get_other_sites_member_ids( $user_ids, $capabilities_key ) {
global $wpdb;

$member_ids = array();
$any_site_capabilities = '/^' . preg_quote( $wpdb->base_prefix, '/' ) . '(\d+_)?capabilities$/';

foreach ( array_chunk( $user_ids, 1000 ) as $chunk ) {
$rows = $wpdb->get_results( $wpdb->prepare(
"SELECT user_id, meta_key FROM {$wpdb->usermeta}
WHERE user_id IN (" . implode( ',', $chunk ) . ") AND meta_key LIKE %s AND meta_key != %s",
$wpdb->esc_like( $wpdb->base_prefix ) . '%capabilities',
$capabilities_key
) );

if ( '' !== $wpdb->last_error ) {
return null;
}

foreach ( $rows as $row ) {
if ( preg_match( $any_site_capabilities, $row->meta_key ) ) {
$member_ids[] = (int) $row->user_id;
}
}
}

return array_values( array_intersect( $user_ids, $member_ids ) );
}

/**
* The ids, logins, slugs and email addresses of the given users, which the object cache keys users by.
*/
private function get_cache_keys_of_users( $user_ids ) {
global $wpdb;

$users = array();

foreach ( array_chunk( $user_ids, 1000 ) as $chunk ) {
$users = array_merge( $users, $wpdb->get_results(
"SELECT ID, user_login, user_nicename, user_email FROM {$wpdb->users} WHERE ID IN (" . implode( ',', $chunk ) . ')'
) );
}

return $users;
}

/**
* Removes the given users from the object cache, as clean_user_cache() does for one user, which would
* load each user and their meta first.
*/
private function clean_users_cache( $users ) {
foreach ( $users as $user ) {
wp_cache_delete( $user->ID, 'users' );
wp_cache_delete( $user->ID, 'user_meta' );
wp_cache_delete( $user->user_login, 'userlogins' );
wp_cache_delete( $user->user_nicename, 'userslugs' );

if ( ! empty( $user->user_email ) ) {
wp_cache_delete( $user->user_email, 'useremail' );
}
}

// Cached user queries, e.g. the users list (WordPress 6.3+).
if ( $users && function_exists( 'wp_cache_set_users_last_changed' ) ) {
wp_cache_set_users_last_changed();
}
}

/**
* Deletes the accounts of the given users, in bulk: there can be thousands, too many for wp_delete_user()
* or wpmu_delete_user(), which would also delete any posts they wrote. So their posts are kept, and
* WordPress's user deletion hooks don't run (nothing in BCC's plugins uses them). Their meta is removed
* afterwards, with the meta of all users that no longer exist.
*/
private function delete_users( $user_ids ) {
global $wpdb;

foreach ( array_chunk( $user_ids, 1000 ) as $chunk ) {
if ( false === $wpdb->query( "DELETE FROM {$wpdb->users} WHERE ID IN (" . implode( ',', $chunk ) . ')' ) ) {
Comment thread
cs1m0n marked this conversation as resolved.
return false;
}
}

return true;
}

/**
* Removes the given users from this site, as WordPress does: their role and user level on the site.
*/
private function remove_users_from_site( $user_ids ) {
global $wpdb;

$blog_prefix = $wpdb->get_blog_prefix();

foreach ( array_chunk( $user_ids, 1000 ) as $chunk ) {
$removed = $wpdb->query( $wpdb->prepare(
"DELETE FROM {$wpdb->usermeta} WHERE meta_key IN (%s, %s) AND user_id IN (" . implode( ',', $chunk ) . ')',
$blog_prefix . 'capabilities',
$blog_prefix . 'user_level'
) );

if ( false === $removed ) {
return false;
}
}

return true;
}

function render_group_selector_field( $args ) { ?>
<div id="<?php echo $args['groupsName']; ?>-container"></div>
<script type="text/javascript">
Expand Down
Loading