Wordpress Multisite: normalen Admins erlauben User zu verwalten (inkl. Plugin)

In Multisite-Installationen von Wordpress wird die Rolle des Admin entmachtet. Sie heißt zwar noch Admin und hat formal noch alle Rechte wie das Pendant in einer Single-Site-Installation, die Benutzerverwaltung fällt aber zum größten Teil den Netzwerk-Administratoren (Super-Admins) zu. (Super-Admin ist im Übrigen keine eigene Rolle, sondern eine simple Liste mit Benutzernamen, gespeichert in der site-option site_admins.) Dem Admin bleibt lediglich die Möglichkeit, neue Nutzer zum Blog hinzuzufügen (wenn die entsprechende Option in den Netzwerk-Einstellungen gesetzt ist). Aber selbst diese Möglichkeit ist für ihn noch um die Option beschnitten, den Nutzer direkt und ohne Aktivierungs-E-Mail hinzuzufügen. Das wird vielleicht irgendwann mal behoben: #20221.

Will man mit einem Wordpress Netzwerk mehrere Nutzergruppen getrennt voneinander auf separaten Blogs verwalten, muss für jede Gruppe mindestens ein Nutzer Netzwerk-Admin sein. Gerade bei steigender Anzahl begrenzt erfahrener Benutzer ist das nicht immer die beste Idee.

Rechte wieder herstellen

Festgeschrieben sind die Restriktionen in der Funktion map_meta_cap() die einige Rechte (Capabilities) nur noch für Netzwerk-Admins zulässt. Ein gleichnamiger Filter erlaubt es aber, den ganzen Spaß wieder rückgängig zu machen. Um der Standardrolle Admin wieder zu erlauben Nutzer des eigenen Blogs zu editieren, reicht es aus, die Capabilities

  • edit_user
  • edit_users
  • manage_network_users

(zurück) zu geben. Natürlich nur, wenn der aktuelle Benutzer diese Capabilities auch besitzt. Allerdings prüfen wir das nicht mit current_user_can() sonst hätten wir das, was Toscho so schön als die Geister beschreibt, die »zwischen zwei gegenüberstehenden Spiegeln wohnen«. Nein, wir sehen uns WP_User::allcaps an. Ein Admin darf natürlich auch nicht einen Super-Admin editieren oder aus dem Blog kanten. Lange Rede, »kurzer« Code:

/**
 * grant 'edit_users' to non-super-admins
 * by default WP inhibits this in map_meta_cap()
 *
 * @wp-hook      map_meta_cap
 * @param  array $caps    bunch of (meta)capabilities
 * @param string $cap     current capability to check for
 * @param    int $user_id
 * @param  array $args
 * @return array
 */
function filter_map_meta_cap( $caps, $cap, $user_id, $args ) {

	# user manage caps which are necessary on multisite
	$user_caps = array(
		'edit_user',
		'edit_users',
		'manage_network_users',
		'remove_user'
	);

	if ( ! in_array( $cap, $user_caps ) )
		return $caps;

	$user = get_user_by( 'id', $user_id );
	if ( ! is_a( $user, 'WP_User' ) )
		return $caps;

	# re-map the capabilities
	switch ( $cap ) {
		case 'edit_user' :
			$cap           = 'edit_users';
			if ( empty( $user->allcaps[ $cap ] ) )
				return $caps; # user can't edit users, nothing to do

			$blog_id      = get_current_blog_id();
			$user_to_edit = $args[ 0 ];
			if ( ! is_user_member_of_blog( $user_to_edit, $blog_id ) 
			  || is_super_admin( $user_to_edit ) 
			)
				return $caps; # the user to edit is not a user of the current blog.
		break;

		case 'remove_user' :
			$user_to_remove = $args[ 0 ];
			if ( is_super_admin( $user_to_remove ) ) {
				$caps[] = 'do_not_allow';

				return $caps;
			}
				$cap = 'remove_users';
		break;

		case 'manage_network_users' :
			# map user-handling caps to 'manage_network_users'
			if ( ! $user->allcaps[ 'edit_users' ]
			  || ! $user->allcaps[ 'promote_users' ]
			  || ! $user->allcaps[ 'remove_users' ]
			)
				return $caps;
		break;

		default :
			if ( empty( $user->allcaps[ $cap ] ) )
				return $caps;
		break;
	}

	# At this point, there can be a 'do_not_allow' inside the $caps array
	# to force revoking rights for super-admins. We shouldn't remove it!
	# But WP_User::has_cap() checks for *all* caps inside the array whether they are
	# exists in WP_User::allcaps so we have to add it
	add_filter(
		'user_has_cap',
		__NAMESPACE__ . '\add_interim_user_cap',
		10,
		3
	);
	$caps[] = $cap;

	return $caps;
}

Einen Haken hat die Sache: map_meta_cap() schreibt eine Capability 'do_not_allow' in das $caps Array. Diese zu entfernen wäre töricht. Allerdings wird dieses Array im nächsten Schritt mit WP_Users::allcaps abgeglichen. Es bedarf also eines weiteren Filters, der für den konkreten Fall do_not_allow in die Liste allcaps aufnimmt. Klingt nach Harakiri, aber der Filter entfernt sich selbst wieder:

/**
 * add the 'do_not_allow' cap to a users 
 * list of capabilities
 *
 * @param array $allcaps
 * @param array $caps (caps to check for)
 * @param mixed $args
 * @return array
 */
function add_interim_user_cap( $allcaps, $caps, $args ) {

	remove_filter(
		'user_has_cap',
		__FUNCTION__,
		10,
		3
	);
	$allcaps[ 'do_not_allow' ] = TRUE;

	return $allcaps;
}

Benutzer direkt und ohne Einladung hinzufügen

Jetzt wirds krude. Denn die Option Benutzer direkt hinzuzufügen ist in wp-admin/user-new.php fest an die Bedingung is_super_admin() gebunden. Kein Filter, nur Hardcodierte HTML-Formularelemente. Die einzige Möglichkeit ist, sich an den Filter der  site-option zu hängen: 'site_option_site_admins', sprich, den Nutzer zum Super-Admin erheben. Das sollte an eine möglichst späte Action im Template geknüpft sein, um nicht die Admin-Bar oder das Admin-Menü zu beeinflussen. Die entsprechenden Hooks sind admin_action_createuser, admin_action_adduser für die Auswertung des Insert-Request und user_new_form_tag für das Formular.

/**
 * allow normal admins to add a user directly 
 * (without sending an activtaion-email) affects user-new.php
 *
 * @link https://core.trac.wordpress.org/ticket/20221
 */
#called on insert request
add_action( 'admin_action_createuser', __NAMESPACE__ . '\temporary_super_admin' );
add_action( 'admin_action_adduser',    __NAMESPACE__ . '\temporary_super_admin' );
#called when showing the insert formular
add_action( 'user_new_form_tag',       __NAMESPACE__ . '\temporary_super_admin' );

function temporary_super_admin() {

	# check for the theoretical permissions
	if ( ! current_user_can( 'create_users' ) && ! current_user_can( 'promote_users' ) )
		return;

	add_filter( 'site_option_site_admins', __NAMESPACE__ . '\add_user_to_admin_list' );
}

/**
 * Add the current user to the list of super-admins
 *
 * @param array $admins
 * @return array
 */
function add_user_to_admin_list( $admins ) {

	$user = wp_get_current_user();
	if ( ! is_a( $user, 'WP_User' ) )
		return;

	$admins[] =$user->user_login;
	return $admins;
}

Die Idee zu diesem Schritt habe ich von Toscho.

Download

Wer bis hierher aufmerksam gelesen hat, wird sich vielleicht über die Namespaces gewundert haben. Die Codebeispiele sind allesamt aus dem zugehörigen Plugin »User edit Users« in dem ich mir den Namspaces nur herum experimentiert habe.

Man sollte sich im Klaren sein, dass sich die Auswirkungen auf alle Filter und Actions erstreckt, die zwischenzeitlich Aufgerufen werden. Die Liste ist ziemlich lang und erhebt keinen Anspruch auf Vollständigkeit.

pre_site_option_site_admins
site_option_site_admins
map_meta_cap
get_user_metadata
attribute_escape
nonce_life
salt
gettext
pre_option_default_role
option_default_role
editable_roles
gettext_with_context
update_footer
pre_site_option_dismissed_update_core
query
default_site_option_dismissed_update_core
site_option_dismissed_update_core
pre_site_transient_update_core
pre_site_option__site_transient_update_core
site_option__site_transient_update_core
site_transient_update_core
network_site_url
network_admin_url
admin_footer_text
print_styles_array
print_late_styles
print_scripts_array
script_loader_src
wp_parse_str
clean_url
print_footer_scripts
pre_site_option_can_compress_scripts
site_option_can_compress_scripts
in_admin_footer
admin_footer
admin_print_footer_scripts
admin_footer-user-new.php
pre_site_option_add_new_users
site_option_add_new_users
user_has_cap
pre_option_siteurl
option_siteurl
site_url
admin_url
sanitize_user
sanitize_email
pre_site_option_illegal_names
site_option_illegal_names
pre_site_option_banned_email_domains
site_option_banned_email_domains
is_email
pre_site_option_limited_email_domains
site_option_limited_email_domains
wpmu_validate_user_signup
pre_user_login
wpmu_signup_user_notification
pre_transient_random_seed
pre_option__transient_random_seed
option__transient_random_seed
transient_random_seed
pre_set_transient_random_seed
sanitize_option__transient_random_seed
pre_update_option__transient_random_seed
random_password
sanitize_title
pre_user_nicename
pre_user_url
pre_kses
pre_user_email
pre_user_display_name
sanitize_text_field
pre_user_nickname
pre_user_first_name
pre_user_last_name
pre_user_description
user_contactmethods
sanitize_user_meta_first_name
update_user_metadata
add_user_metadata
sanitize_user_meta_last_name
sanitize_user_meta_nickname
sanitize_user_meta_description
sanitize_user_meta_rich_editing
sanitize_user_meta_comment_shortcuts
sanitize_user_meta_admin_color
sanitize_user_meta_use_ssl
sanitize_user_meta_show_admin_bar_front
sanitize_user_meta_wp_2_capabilities
sanitize_user_meta_wp_2_user_level
sanitize_user_meta_dismissed_wp_pointers
delete_user_metadata
pre_site_option_registrationnotification
site_option_registrationnotification
pre_site_option_admin_email
site_option_admin_email
wpmu_welcome_user_notification
pre_option_wp_user_roles
option_wp_user_roles
get_blogs_of_user
sanitize_user_meta_primary_blog
sanitize_user_meta_source_domain
pre_option_wp_2_user_roles
option_wp_2_user_roles
pre_option_blogname
option_blogname
blog_option_blogname
blog_option_siteurl
pre_option_post_count
default_option_post_count
blog_option_post_count
blog_details
wp_redirect
check_admin_referer
update_option
update_option__transient_random_seed
updated_option
set_transient__transient_random_seed
setted_transient
add_user_meta
added_user_meta
set_user_role
user_register
delete_user_meta
deleted_user_meta
wpmu_new_user
switch_blog
remove_user_from_blog
update_user_meta
updated_user_meta
add_user_to_blog
wpmu_activate_user

Kommentare

Es wurden noch keine Kommentarte zu diesem Artikel geschrieben.

Fragen, Ideen oder Kritik? – Hier ist Platz dafür!

Dein Kommentar

Um ein Kommentar abzugeben, reicht der Text im Kommentarfeld. Die Angabe eines Namens wäre nett, ist aber nicht erforderlich.

Du darfst folgenden HTML-Code verwenden, musst aber nicht:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>