WordPress deletes custom roles on user profile update (solved)

OVERVIEW

WordPress allows you to add custom security roles and capabilities for users. Unfortunately, any time a user profile is updated, either by a user or an administrator, custom roles that were assigned to a user could be lost. The following example code demonstrates a work-around to restore custom roles after a user profile update.

BACKGROUND

The problem occurs because the code that processes a form submission from the User Profile editor page only allows a single user role to be passed to the wp_update_user() function. If you created a user with a primary role of Subscriber, but also have a plugin that assigns an additional security role to that user, it will be lost every time the Update button is clicked on the User Profile editor page.

The work-around uses three hooks to capture and restore custom roles. The ‘edit_user_profile’ and ‘show_user_profile’ action hooks are called when the User Profile editor page is being generated for display. The ‘edit_user_profile’ action is fired when someone is editing a user profile that is not their own. The ‘show_user_profile’ action fires when you are viewing your own profile.

The third action hook, ‘user_profile_update_errors’, fires inside WordPress’ edit_user() function at a good time to capture a list of user roles before they get overwritten.

Here is the sequence of events in a typical user profile update with the work-around:

  • User Profile editor page starts to load.
  • Work-around function user_profile_loading() is called from inside the /wp-admin/user-edit.php file when the User Profile editor form is loading. The user_profile_loading() function checks for a WordPress transient to see if the profile of the user being loaded was just updated. On the first time through, no transient data exist, so the User Profile editor form continues to load normally.
  • The Update button is clicked on the User Profile editor form. The form data is submitted for processing.
  • Work-around function user_profile_updating() is then called from inside the edit_user() function inside the /wp-admin/includes/user.php file. The user_profile_updating() function saves a copy of all the roles of the user being updated just before those roles get overwritten. The copy of these roles are saved in a transient.
  • WordPress finishes saving the user profile data, destroying our custom role in the process. WordPress then redirects the user back to the User Profile editor page to show the updated user data in the form.
  • Work-around function user_profile_loading() gets called again. This time the function finds data in the transient indicating that the user profile has just been updated. The transient data also contains a copy of the original user roles assigned to that user. The function restores the custom roles that it recognizes in that data and ignores the rest. The function then deletes the transient data for that user so it won’t be used again.

THE SOLUTION

In this code sample, the custom user role is named, ‘my_custom_role’ and is the only user role that will be recognized and restored in this process.

<?php
/** Add hooks to restore user roles after a user profile update */
add_action( 'edit_user_profile', 'user_profile_loading', 10, 1 );
add_action( 'show_user_profile', 'user_profile_loading', 10, 1 );
add_action( 'user_profile_update_errors', 'user_profile_updating', 10, 3 );

/**
 * WordPress function wp_update_user() destroys all but
 * one of the assigned user roles during a profile update.
 * Restore user roles after profile update, if needed.
 *
 * This is called on first view of a user profile and
 * also after a user profile is updated because of a
 * redirect to the user profile view on successful update.
 *
 * @param WP_User $user WP_User object of user being displayed.
 */
function user_profile_loading( $user ) {
	/**
	 * Get transient data on user profile changes.
	 */
	$trans = get_transient( 'saved_user_roles' );
	if ( empty( $trans ) ) {
		return; // No data to act on, return.
	}

	/**
	 * Make a copy of the User ID for efficiency.
	 *
	 * @var int $uid
	 */
	$uid = $user->ID;
	if ( empty( $trans[ $uid ] ) ) {
		return;  // No transient data saved for this user, return.
	}

	/**
	 * Get a copy of user roles from transient array for efficiency.
	 *
	 * @var array $roles
	 */
	$roles = $trans[ $uid ];

	/**
	 * RESTORE MISSING ROLE IF NEEDED.
	 */
	if ( in_array( 'my_custom_role', $roles ) ) {
		$user->add_role( 'my_custom_role' );
	}
	/**
	 * Add more role checking and restoration if needed.
	 */

	/**
	 * Get the latest transient data in case it changed.
	 * Function is not atomic, try to be as close as possible.
	 */
	$trans = get_transient( 'saved_user_roles' );

	/**
	 * Delete transient data for the user roles that were restored just now.
	 */
	unset( $trans[ $uid ] );

	/**
	 * Delete transient if empty.
	 * Update if other data is still in it.
	 */
	if ( empty( $trans ) ) {
		delete_transient( 'saved_user_roles' );
	} else {
		set_transient( 'saved_user_roles', $trans );
	}
}

/**
 * WordPress function wp_update_user() destroys all
 * but one of the assigned user roles during a profile update.
 * Save plugin user roles during user profile update.
 * Restore after redirect back to user profile view on successful update.
 *
 * @param WP_Error $errors Collection of Errors
 * @param boolean $update True if user profile updated, false if new user created.
 * @param stdClass $user User profile data, not a WP_User object.
 */
function user_profile_updating( &$errors, $update, &$user ) {
	/**
	 * If this is not an update, return.
	 */
	if ( !$update ) {
		return;
	}

	/**
	 * If there are errors, don't get in the way, return.
	 */
	if ( $errors->has_errors() ) {
		return;
	}

	/**
	 * Get the unmodified WP_User before it gets mangled.
	 */
	$uid		 = $user->ID;
	$user_temp	 = new WP_User( $uid );

	/**
	 * Get our transient user data.
	 */
	$trans = get_transient( 'saved_user_roles' );
	if ( empty( $trans ) ) {
		$trans = array(); // Make a new array if it doesn't exist.
	}

	/**
	 * Save the information we need to restore the user roles later.
	 */
	$trans[ $uid ] = $user_temp->roles;

	/**
	 * Save the transient data.
	 */
	set_transient( 'saved_user_roles', $trans);
}

NOTE

There may be a remote possibility of saved user role transient data surviving forever since no expiration is specified. If this is a concern, you could add an expiration time to the set_transient() call to something reasonable. For example, changing set_transient( ‘saved_user_roles’, $trans );   to   set_transient( ‘saved_user_roles’, $trans, 86400 ); would automatically delete the saved user role transient data after 24 hours.

CONCLUSION

It would be better if WordPress itself could properly handle user profile updates on users with more than one security role. Until improvements are made, the work-around illustrated above should get the job done.