Skip to content
Open
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
48 changes: 36 additions & 12 deletions src/js/_enqueues/lib/user-suggest.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/**
* Suggests users in a multisite environment.
* Suggests users in site admin and multisite environments.
*
* For input fields where the admin can select a user based on email or
* username, this script shows an autocompletion menu for these inputs. Should
* only be used in a multisite environment. Only users in the currently active
* site are shown.
* For input fields where the admin can select a user by searching their name,
* login, or email, this script shows an autocompletion menu. On multisite,
* only users in the currently active site are shown for 'search' type requests.
*
* @since 3.4.0
* @output wp-admin/js/user-suggest.js
Expand Down Expand Up @@ -36,19 +35,29 @@
* - data-autocomplete-type (add, search)
* The action that is going to be performed: search for existing users
* or add a new one. Default: add
* - data-autocomplete-field (user_login, user_email)
* - data-autocomplete-field (user_login, user_email, user_id)
* The field that is returned as the value for the suggestion.
* When set to 'user_id', the input is expected to have an adjacent
* '.wp-suggest-user-helper' hidden input that stores the numeric ID
* while the visible input shows the display label.
* Default: user_login
* - data-autocomplete-label
* A template string with {{tokens}} to build each result's display label.
* Supported tokens: {{user_login}}, {{user_email}}, {{display_name}}, {{user_id}}.
* Default: empty (server returns display_name).
*
* @see wp-admin/includes/admin-actions.php:wp_ajax_autocomplete_user()
* @see wp-admin/includes/ajax-actions.php:wp_ajax_autocomplete_user()
*/
$( '.wp-suggest-user' ).each( function(){
var $this = $( this ),
autocompleteType = ( typeof $this.data( 'autocompleteType' ) !== 'undefined' ) ? $this.data( 'autocompleteType' ) : 'add',
autocompleteField = ( typeof $this.data( 'autocompleteField' ) !== 'undefined' ) ? $this.data( 'autocompleteField' ) : 'user_login';
$( '.wp-suggest-user' ).each( function() {
var $this = $( this ),
autocompleteType = ( typeof $this.data( 'autocompleteType' ) !== 'undefined' ) ? $this.data( 'autocompleteType' ) : 'add',
autocompleteField = ( typeof $this.data( 'autocompleteField' ) !== 'undefined' ) ? $this.data( 'autocompleteField' ) : 'user_login',
autocompleteLabel = ( typeof $this.data( 'autocompleteLabel' ) !== 'undefined' ) ? $this.data( 'autocompleteLabel' ) : '',
// True when using user_id field with a sibling helper input.
hasHelper = ( 'user_id' === autocompleteField && $this.next( '.wp-suggest-user-helper' ).length > 0 );

$this.autocomplete({
source: ajaxurl + '?action=autocomplete-user&autocomplete_type=' + autocompleteType + '&autocomplete_field=' + autocompleteField + id,
source: ajaxurl + '?action=autocomplete-user&autocomplete_type=' + autocompleteType + '&autocomplete_field=' + autocompleteField + '&autocomplete_label=' + encodeURIComponent( autocompleteLabel ) + id,
delay: 500,
minLength: 2,
position: position,
Expand All @@ -57,6 +66,21 @@
},
close: function() {
$( this ).removeClass( 'open' );
},
focus: function( e, ui ) {
if ( hasHelper ) {
// Show the display label while navigating, not the raw ID value.
$( this ).val( ui.item.label );
return false;
}
},
select: function( e, ui ) {
if ( hasHelper ) {
// Store the user ID in the hidden helper; show the label in the text input.
$( this ).next( '.wp-suggest-user-helper' ).val( ui.item.value );
$( this ).val( ui.item.label );
return false;
}
}
});
});
Expand Down
172 changes: 131 additions & 41 deletions src/wp-admin/includes/ajax-actions.php
Original file line number Diff line number Diff line change
Expand Up @@ -284,80 +284,170 @@ function wp_ajax_oembed_cache() {
/**
* Handles user autocomplete via AJAX.
*
* Works on both single-site and multisite installs. On multisite, the 'add'
* type searches the full network and is restricted to network admins; the
* 'search' type is restricted to users already on the current site.
* On single-site, only the 'search' type is supported and requires
* the 'list_users' capability.
*
* @since 3.4.0
* @since 7.1.0 Added single-site support, the `user_id` autocomplete field,
* label template tokens, and the `autocomplete_user_results` filter.
*/
function wp_ajax_autocomplete_user() {
if ( ! is_multisite() || ! current_user_can( 'promote_users' ) || wp_is_large_network( 'users' ) ) {
wp_die( -1 );
}
/*
* Validate the minimum search term length before anything else.
* The same minimum is enforced in JS via the minLength option.
*/
$term = isset( $_REQUEST['term'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['term'] ) ) : '';

/** This filter is documented in wp-admin/user-new.php */
if ( ! current_user_can( 'manage_network_users' ) && ! apply_filters( 'autocomplete_users_for_site_admins', false ) ) {
/**
* Filters the minimum search term length for user autocomplete.
*
* The same minimum should be mirrored in the JavaScript minLength option.
*
* @since 7.1.0
*
* @param int $length Minimum number of characters required. Default 2.
* @param string $context Context for the autocomplete search. Currently 'users'.
*/
if ( strlen( $term ) < apply_filters( 'autocomplete_term_length', 2, 'users' ) ) {
wp_die( -1 );
}

$return = array();

/*
* Check the type of request.
* Current allowed values are `add` and `search`.
* Allowed values are `add` (multisite only) and `search`.
*/
if ( isset( $_REQUEST['autocomplete_type'] ) && 'search' === $_REQUEST['autocomplete_type'] ) {
$type = $_REQUEST['autocomplete_type'];
} else {
$type = 'add';
}
$type = ( isset( $_REQUEST['autocomplete_type'] ) && 'search' === $_REQUEST['autocomplete_type'] )
? 'search'
: 'add';

/*
* Check the desired field for value.
* Current allowed values are `user_email` and `user_login`.
* Determine the user field to return as the suggestion value.
* Allowed values are `user_email`, `user_login`, and `user_id`.
*/
if ( isset( $_REQUEST['autocomplete_field'] ) && 'user_email' === $_REQUEST['autocomplete_field'] ) {
$field = $_REQUEST['autocomplete_field'];
$requested_field = isset( $_REQUEST['autocomplete_field'] ) ? $_REQUEST['autocomplete_field'] : '';
if ( 'user_email' === $requested_field ) {
$field = 'user_email';
} elseif ( 'user_id' === $requested_field ) {
$field = 'ID';
} else {
$field = 'user_login';
}

// Exclude current users of this blog.
if ( isset( $_REQUEST['site_id'] ) ) {
$id = absint( $_REQUEST['site_id'] );
/*
* Resolve the label template. Supported tokens:
* {{user_login}}, {{user_email}}, {{display_name}}, {{user_id}}.
*/
$label_template = ( isset( $_REQUEST['autocomplete_label'] ) && '' !== $_REQUEST['autocomplete_label'] )
? sanitize_text_field( wp_unslash( $_REQUEST['autocomplete_label'] ) )
: '{{display_name}}';

$blog_id = false;
$include_blog_users = array();
$exclude_blog_users = array();
$search_columns = array( 'user_login', 'user_nicename', 'display_name' );

if ( is_multisite() && 'add' === $type ) {
/*
* Adding a user to a site requires network-level permissions and should
* not run on very large networks where the search would be slow.
*/
if ( ! current_user_can( 'promote_users' ) || wp_is_large_network( 'users' ) ) {
wp_die( -1 );
}

/** This filter is documented in wp-admin/user-new.php */
if ( ! current_user_can( 'manage_network_users' ) && ! apply_filters( 'autocomplete_users_for_site_admins', false ) ) {
wp_die( -1 );
}

// Search the full network; exclude users already on the target site.
$site_id = isset( $_REQUEST['site_id'] ) ? absint( $_REQUEST['site_id'] ) : get_current_blog_id();
$exclude_blog_users = get_users(
array(
'blog_id' => $site_id,
'fields' => 'ID',
)
);

// Super-admins may search by email as well.
$search_columns[] = 'user_email';
} else {
$id = get_current_blog_id();
}
/*
* Single-site search, or multisite 'search' type.
* Requires the ability to list users on this site.
*/
if ( ! current_user_can( 'list_users' ) ) {
wp_die( -1 );
}

$include_blog_users = ( 'search' === $type ? get_users(
array(
'blog_id' => $id,
'fields' => 'ID',
)
) : array() );
$blog_id = get_current_blog_id();

$exclude_blog_users = ( 'add' === $type ? get_users(
array(
'blog_id' => $id,
'fields' => 'ID',
)
) : array() );
if ( is_multisite() ) {
// Restrict results to users already on this site.
$include_blog_users = get_users(
array(
'blog_id' => $blog_id,
'fields' => 'ID',
)
);
}

// Include email in search columns only for users who can edit others.
if ( current_user_can( 'edit_users' ) ) {
$search_columns[] = 'user_email';
}
}

// Email tokens in the label should only be visible to users who can edit others.
if ( ! current_user_can( 'edit_users' ) ) {
$label_template = str_replace( '{{user_email}}', '', $label_template );
}

$users = get_users(
array(
'blog_id' => false,
'search' => '*' . $_REQUEST['term'] . '*',
'blog_id' => $blog_id,
'search' => '*' . $term . '*',
'include' => $include_blog_users,
'exclude' => $exclude_blog_users,
'search_columns' => array( 'user_login', 'user_nicename', 'user_email' ),
'search_columns' => $search_columns,
'number' => 20,
)
);

$return = array();

foreach ( $users as $user ) {
// Replace supported tokens in the label template.
$label = $label_template;
foreach ( array( 'user_login', 'user_email', 'display_name' ) as $token ) {
$label = str_replace( '{{' . $token . '}}', $user->$token, $label );
}
$label = str_replace( '{{user_id}}', $user->ID, $label );

if ( '' === trim( $label ) ) {
$label = '(' . $user->user_login . ')';
}

$return[] = array(
/* translators: 1: User login, 2: User email address. */
'label' => sprintf( _x( '%1$s (%2$s)', 'user autocomplete result' ), $user->user_login, $user->user_email ),
'value' => $user->$field,
'label' => esc_html( $label ),
'value' => ( 'ID' === $field ) ? $user->ID : $user->$field,
);
}

wp_die( wp_json_encode( $return ) );
/**
* Filters the user autocomplete results array.
*
* @since 7.1.0
*
* @param array $return Array of result objects with 'label' and 'value' keys.
* @param string $term The sanitized search term.
*/
$return = apply_filters( 'autocomplete_user_results', $return, $term );

wp_send_json( $return );
}

/**
Expand Down
7 changes: 6 additions & 1 deletion src/wp-admin/users.php
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,12 @@
wp_delete_user( $id );
break;
case 'reassign':
wp_delete_user( $id, $_REQUEST['reassign_user'] );
$reassign_id = isset( $_REQUEST['reassign_user'] ) ? absint( $_REQUEST['reassign_user'] ) : 0;
if ( $reassign_id > 0 && $reassign_id !== $id ) {
wp_delete_user( $id, $reassign_id );
} else {
wp_delete_user( $id );
}
break;
}

Expand Down
Loading
Loading