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
6 changes: 5 additions & 1 deletion plugins/bcc-login/bcc-login.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
require_once( 'includes/class-bcc-storage.php' );
require_once( 'includes/class-bcc-notifications.php' );
require_once( 'includes/class-exclusive-lock.php' );
require_once( 'includes/class-bcc-login-cleanup.php' );

class BCC_Login {
/**
Expand All @@ -51,7 +52,8 @@ class BCC_Login {
private BCC_Coreapi_Client $_coreapi;
private BCC_Notifications $_notifications;
private BCC_Storage $_storage;

private BCC_Login_Cleanup $_cleanup;

/**
* Initialize the plugin.
*/
Expand All @@ -73,6 +75,7 @@ private function __construct(){
$this->_feed = new BCC_Login_Feed( $this->_settings, $this->_client );
$this->_updater = new BCC_Login_Updater( $this->plugin, $this->plugin_slug, $this->plugin_version, $this->plugin_name );
$this->_notifications = new BCC_Notifications( $this->_settings, $this->_coreapi );
$this->_cleanup = new BCC_Login_Cleanup();

if (!empty($this->_settings->site_groups) || !empty($this->_settings->full_content_access_groups)) {
$this->_coreapi->ensure_subscription_to_person_updates();
Expand Down Expand Up @@ -265,6 +268,7 @@ static function activate_plugin() {
* Called when plugin is deactivated
*/
static function deactivate_plugin() {
BCC_Login_Cleanup::unschedule();
flush_rewrite_rules();
}

Expand Down
69 changes: 69 additions & 0 deletions plugins/bcc-login/includes/class-bcc-login-cleanup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

/**
* Daily background task (WP-Cron) that deletes data left behind by 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';

function __construct() {
add_action( self::CRON_HOOK, array( __CLASS__, 'run' ) );

// WordPress doesn't run activation hooks on plugin updates,
// so the task is scheduled by the first request that loads this code.
add_action( 'init', array( $this, 'schedule' ) );
}

function schedule() {
if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
wp_schedule_event( time(), 'daily', self::CRON_HOOK );
}
}

static function unschedule() {
wp_clear_scheduled_hook( self::CRON_HOOK );
}

static function run() {
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 tokens and claims that the old OpenID Connect Generic plugin stored as user meta.
*
* @return int|false The number of deleted rows, or false if not all of them could be deleted.
*/
static function delete_legacy_user_meta() {
global $wpdb;

$legacy_meta_keys = $wpdb->get_results(
"SELECT meta_key, COUNT(*) AS count
FROM {$wpdb->usermeta}
WHERE meta_key LIKE 'openid-connect-generic%'
GROUP BY meta_key"
Comment thread
cs1m0n marked this conversation as resolved.
);

$deleted = 0;
foreach ( (array) $legacy_meta_keys as $legacy_meta_key ) {
// Unlike a plain DELETE query, this also clears the cached meta of the users.
delete_metadata( 'user', 0, $legacy_meta_key->meta_key, '', true );
$deleted += (int) $legacy_meta_key->count;
}

$remaining = $wpdb->get_var(
"SELECT COUNT(*) FROM {$wpdb->usermeta} WHERE meta_key LIKE 'openid-connect-generic%'"
);

// Null means the query failed.
if ( null === $remaining || (int) $remaining > 0 ) {
return false;
}

return $deleted;
}
}
74 changes: 70 additions & 4 deletions plugins/bcc-login/includes/class-bcc-login-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,9 @@ function render_delete_subscribers_button () {
<hr>
<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>
Comment thread
cs1m0n marked this conversation as resolved.
<form method="post">
<input type="hidden" name="_wp_http_referer" value="<?php echo add_query_arg( 'delete_subscribers', 'true', wp_get_referer() ) ?>">
<?php wp_nonce_field( 'delete_subscribers', 'delete_subscribers_nonce' ); ?>
<?php submit_button('Delete subscribers', 'delete', 'delete_subscribers', false, array(
'onclick' => 'return confirm("Are you sure you want to delete all the subscribers?");'
)); ?>
Expand All @@ -667,7 +668,13 @@ function render_delete_subscribers_button () {
* Handles deletion of subscribers
*/
function delete_subscribers_handler() {
if ( strpos(wp_get_referer(), 'delete_subscribers=true') !== false) {
// Only run for the form above, submitted by an administrator. The nonce stops other sites
// from submitting it on behalf of a logged-in administrator.
if (
isset( $_POST['delete_subscribers'], $_POST['delete_subscribers_nonce'] ) &&
wp_verify_nonce( $_POST['delete_subscribers_nonce'], 'delete_subscribers' ) &&
current_user_can( 'administrator' )
) {
global $wpdb;

$sql = "DELETE {$wpdb->prefix}users, {$wpdb->prefix}usermeta
Expand All @@ -677,12 +684,71 @@ function delete_subscribers_handler() {
meta_key = '{$wpdb->prefix}capabilities' AND
meta_value LIKE 'a:1:{s:10:\"subscriber\";b:1;}'";

$result = $wpdb->get_results($sql);
$wpdb->query($sql);

// 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);

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' ),
'error'
);
}

// Remove tokens and claims stored by the old OpenID Connect Generic plugin for the remaining users (e.g. admins and editors).
$legacy_meta_deleted = BCC_Login_Cleanup::delete_legacy_user_meta();

if ( false === $legacy_meta_deleted ) {
add_settings_error(
'subscribers_deleted',
'legacy_user_meta_not_deleted',
__( 'Not all OpenID Connect Generic user meta could be deleted.', 'bcc-login' ),
'error'
);
}

// Remove options and transients left behind by the old OpenID Connect Generic plugin (states, logs, version).
// 'openid_connect_generic_settings' is kept, as it's still used as fallback for the client id and secret.
$legacy_options = $wpdb->get_col(
"SELECT option_name FROM {$wpdb->prefix}options WHERE option_name LIKE '%openid-connect-generic%'"
);

foreach ( $legacy_options as $legacy_option ) {
delete_option( $legacy_option );
}

// Remove expired transients, e.g. the oidc_* tokens of ended sessions. WP does this daily via WP-Cron,
// 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.' ) );

if ( false !== $orphaned_meta_deleted ) {
$messages[] = sprintf( __( 'Removed %d user meta rows of deleted users.', 'bcc-login' ), $orphaned_meta_deleted );
}

if ( false !== $legacy_meta_deleted ) {
$messages[] = sprintf( __( 'Removed %d OpenID Connect Generic user meta rows.', 'bcc-login' ), $legacy_meta_deleted );
}

$messages[] = sprintf( __( 'Removed %d OpenID Connect Generic options.', 'bcc-login' ), count( $legacy_options ) );
$messages[] = __( 'Removed expired transients.', 'bcc-login' );

add_settings_error(
'subscribers_deleted',
'subscribers_deleted',
__( 'All subscribers were successfully deleted.' ) . ' – ' . implode(', ', $result),
implode( ' ', $messages ),
'success'
);
}
Expand Down
Loading