Skip to content
Merged
Show file tree
Hide file tree
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
95 changes: 94 additions & 1 deletion plugins/bcc-login/includes/class-bcc-login-cleanup.php
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
<?php

/**
* Daily background task (WP-Cron) that deletes data left behind by the old OpenID Connect Generic plugin.
* Daily background task (WP-Cron) that deletes user data left behind by older versions of the plugin:
* the user meta of users that no longer exist (older versions of "Delete subscribers" only removed the
* users and their role), and the tokens and claims of the old OpenID Connect Generic plugin.
*
* It keeps running rather than running once, so the data is also removed when it comes back,
* e.g. from a restored backup or a database pushed from another environment.
*/
class BCC_Login_Cleanup {
const CRON_HOOK = 'bcc_login_cleanup';
const MISSING_USERS_OPTION = 'bcc_login_missing_users';

function __construct() {
add_action( self::CRON_HOOK, array( __CLASS__, 'run' ) );
Expand All @@ -28,11 +31,101 @@ static function unschedule() {
}

static function run() {
if ( false === self::delete_orphaned_user_meta( true ) ) {
error_log( 'BCC Login: Could not delete the user meta of deleted users, retrying tomorrow.' );
}

if ( false === self::delete_legacy_user_meta() ) {
error_log( 'BCC Login: Could not delete the user meta of the OpenID Connect Generic plugin, retrying tomorrow.' );
}
}

/**
* Deletes the user meta 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.
*
* @param bool $only_missing_before Only delete the meta of users that were already missing at a call at least
* 12 hours earlier (normally the previous day's run). The daily task uses this
* because a database import loads wp_usermeta before wp_users, so while one is
* running every user seems to be missing. An import doesn't last that long, and
* the time check also covers WP-Cron running the task twice at once.
* @return int|false The number of deleted rows, or false if a query failed (rows of earlier batches stay deleted).
*/
static function delete_orphaned_user_meta( $only_missing_before = false ) {
global $wpdb;

// WordPress never stores meta for user id 0, so rows with it aren't left behind by a deleted user.
$user_ids = $wpdb->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.
*
Expand Down
38 changes: 27 additions & 11 deletions plugins/bcc-login/includes/class-bcc-login-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -677,30 +677,42 @@ 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
WHERE
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();
Comment thread
cs1m0n marked this conversation as resolved.

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'
);
}
Expand Down Expand Up @@ -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 );
Expand Down
Loading