Skip to content

Commit 9e51c6e

Browse files
committed
Users: Add autocomplete support to wp_dropdown_users()
1 parent 609f25f commit 9e51c6e

6 files changed

Lines changed: 1014 additions & 61 deletions

File tree

src/js/_enqueues/lib/user-suggest.js

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
/**
2-
* Suggests users in a multisite environment.
2+
* Suggests users in site admin and multisite environments.
33
*
4-
* For input fields where the admin can select a user based on email or
5-
* username, this script shows an autocompletion menu for these inputs. Should
6-
* only be used in a multisite environment. Only users in the currently active
7-
* site are shown.
4+
* For input fields where the admin can select a user by searching their name,
5+
* login, or email, this script shows an autocompletion menu. On multisite,
6+
* only users in the currently active site are shown for 'search' type requests.
87
*
98
* @since 3.4.0
109
* @output wp-admin/js/user-suggest.js
@@ -36,19 +35,29 @@
3635
* - data-autocomplete-type (add, search)
3736
* The action that is going to be performed: search for existing users
3837
* or add a new one. Default: add
39-
* - data-autocomplete-field (user_login, user_email)
38+
* - data-autocomplete-field (user_login, user_email, user_id)
4039
* The field that is returned as the value for the suggestion.
40+
* When set to 'user_id', the input is expected to have an adjacent
41+
* '.wp-suggest-user-helper' hidden input that stores the numeric ID
42+
* while the visible input shows the display label.
4143
* Default: user_login
44+
* - data-autocomplete-label
45+
* A template string with {{tokens}} to build each result's display label.
46+
* Supported tokens: {{user_login}}, {{user_email}}, {{display_name}}, {{user_id}}.
47+
* Default: empty (server returns display_name).
4248
*
43-
* @see wp-admin/includes/admin-actions.php:wp_ajax_autocomplete_user()
49+
* @see wp-admin/includes/ajax-actions.php:wp_ajax_autocomplete_user()
4450
*/
45-
$( '.wp-suggest-user' ).each( function(){
46-
var $this = $( this ),
47-
autocompleteType = ( typeof $this.data( 'autocompleteType' ) !== 'undefined' ) ? $this.data( 'autocompleteType' ) : 'add',
48-
autocompleteField = ( typeof $this.data( 'autocompleteField' ) !== 'undefined' ) ? $this.data( 'autocompleteField' ) : 'user_login';
51+
$( '.wp-suggest-user' ).each( function() {
52+
var $this = $( this ),
53+
autocompleteType = ( typeof $this.data( 'autocompleteType' ) !== 'undefined' ) ? $this.data( 'autocompleteType' ) : 'add',
54+
autocompleteField = ( typeof $this.data( 'autocompleteField' ) !== 'undefined' ) ? $this.data( 'autocompleteField' ) : 'user_login',
55+
autocompleteLabel = ( typeof $this.data( 'autocompleteLabel' ) !== 'undefined' ) ? $this.data( 'autocompleteLabel' ) : '',
56+
// True when using user_id field with a sibling helper input.
57+
hasHelper = ( 'user_id' === autocompleteField && $this.next( '.wp-suggest-user-helper' ).length > 0 );
4958

5059
$this.autocomplete({
51-
source: ajaxurl + '?action=autocomplete-user&autocomplete_type=' + autocompleteType + '&autocomplete_field=' + autocompleteField + id,
60+
source: ajaxurl + '?action=autocomplete-user&autocomplete_type=' + autocompleteType + '&autocomplete_field=' + autocompleteField + '&autocomplete_label=' + encodeURIComponent( autocompleteLabel ) + id,
5261
delay: 500,
5362
minLength: 2,
5463
position: position,
@@ -57,6 +66,21 @@
5766
},
5867
close: function() {
5968
$( this ).removeClass( 'open' );
69+
},
70+
focus: function( e, ui ) {
71+
if ( hasHelper ) {
72+
// Show the display label while navigating, not the raw ID value.
73+
$( this ).val( ui.item.label );
74+
return false;
75+
}
76+
},
77+
select: function( e, ui ) {
78+
if ( hasHelper ) {
79+
// Store the user ID in the hidden helper; show the label in the text input.
80+
$( this ).next( '.wp-suggest-user-helper' ).val( ui.item.value );
81+
$( this ).val( ui.item.label );
82+
return false;
83+
}
6084
}
6185
});
6286
});

src/wp-admin/includes/ajax-actions.php

Lines changed: 131 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -284,80 +284,170 @@ function wp_ajax_oembed_cache() {
284284
/**
285285
* Handles user autocomplete via AJAX.
286286
*
287+
* Works on both single-site and multisite installs. On multisite, the 'add'
288+
* type searches the full network and is restricted to network admins; the
289+
* 'search' type is restricted to users already on the current site.
290+
* On single-site, only the 'search' type is supported and requires
291+
* the 'list_users' capability.
292+
*
287293
* @since 3.4.0
294+
* @since 7.1.0 Added single-site support, the `user_id` autocomplete field,
295+
* label template tokens, and the `autocomplete_user_results` filter.
288296
*/
289297
function wp_ajax_autocomplete_user() {
290-
if ( ! is_multisite() || ! current_user_can( 'promote_users' ) || wp_is_large_network( 'users' ) ) {
291-
wp_die( -1 );
292-
}
298+
/*
299+
* Validate the minimum search term length before anything else.
300+
* The same minimum is enforced in JS via the minLength option.
301+
*/
302+
$term = isset( $_REQUEST['term'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['term'] ) ) : '';
293303

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

299-
$return = array();
300-
301318
/*
302319
* Check the type of request.
303-
* Current allowed values are `add` and `search`.
320+
* Allowed values are `add` (multisite only) and `search`.
304321
*/
305-
if ( isset( $_REQUEST['autocomplete_type'] ) && 'search' === $_REQUEST['autocomplete_type'] ) {
306-
$type = $_REQUEST['autocomplete_type'];
307-
} else {
308-
$type = 'add';
309-
}
322+
$type = ( isset( $_REQUEST['autocomplete_type'] ) && 'search' === $_REQUEST['autocomplete_type'] )
323+
? 'search'
324+
: 'add';
310325

311326
/*
312-
* Check the desired field for value.
313-
* Current allowed values are `user_email` and `user_login`.
327+
* Determine the user field to return as the suggestion value.
328+
* Allowed values are `user_email`, `user_login`, and `user_id`.
314329
*/
315-
if ( isset( $_REQUEST['autocomplete_field'] ) && 'user_email' === $_REQUEST['autocomplete_field'] ) {
316-
$field = $_REQUEST['autocomplete_field'];
330+
$requested_field = isset( $_REQUEST['autocomplete_field'] ) ? $_REQUEST['autocomplete_field'] : '';
331+
if ( 'user_email' === $requested_field ) {
332+
$field = 'user_email';
333+
} elseif ( 'user_id' === $requested_field ) {
334+
$field = 'ID';
317335
} else {
318336
$field = 'user_login';
319337
}
320338

321-
// Exclude current users of this blog.
322-
if ( isset( $_REQUEST['site_id'] ) ) {
323-
$id = absint( $_REQUEST['site_id'] );
339+
/*
340+
* Resolve the label template. Supported tokens:
341+
* {{user_login}}, {{user_email}}, {{display_name}}, {{user_id}}.
342+
*/
343+
$label_template = ( isset( $_REQUEST['autocomplete_label'] ) && '' !== $_REQUEST['autocomplete_label'] )
344+
? sanitize_text_field( wp_unslash( $_REQUEST['autocomplete_label'] ) )
345+
: '{{display_name}}';
346+
347+
$blog_id = false;
348+
$include_blog_users = array();
349+
$exclude_blog_users = array();
350+
$search_columns = array( 'user_login', 'user_nicename', 'display_name' );
351+
352+
if ( is_multisite() && 'add' === $type ) {
353+
/*
354+
* Adding a user to a site requires network-level permissions and should
355+
* not run on very large networks where the search would be slow.
356+
*/
357+
if ( ! current_user_can( 'promote_users' ) || wp_is_large_network( 'users' ) ) {
358+
wp_die( -1 );
359+
}
360+
361+
/** This filter is documented in wp-admin/user-new.php */
362+
if ( ! current_user_can( 'manage_network_users' ) && ! apply_filters( 'autocomplete_users_for_site_admins', false ) ) {
363+
wp_die( -1 );
364+
}
365+
366+
// Search the full network; exclude users already on the target site.
367+
$site_id = isset( $_REQUEST['site_id'] ) ? absint( $_REQUEST['site_id'] ) : get_current_blog_id();
368+
$exclude_blog_users = get_users(
369+
array(
370+
'blog_id' => $site_id,
371+
'fields' => 'ID',
372+
)
373+
);
374+
375+
// Super-admins may search by email as well.
376+
$search_columns[] = 'user_email';
324377
} else {
325-
$id = get_current_blog_id();
326-
}
378+
/*
379+
* Single-site search, or multisite 'search' type.
380+
* Requires the ability to list users on this site.
381+
*/
382+
if ( ! current_user_can( 'list_users' ) ) {
383+
wp_die( -1 );
384+
}
327385

328-
$include_blog_users = ( 'search' === $type ? get_users(
329-
array(
330-
'blog_id' => $id,
331-
'fields' => 'ID',
332-
)
333-
) : array() );
386+
$blog_id = get_current_blog_id();
334387

335-
$exclude_blog_users = ( 'add' === $type ? get_users(
336-
array(
337-
'blog_id' => $id,
338-
'fields' => 'ID',
339-
)
340-
) : array() );
388+
if ( is_multisite() ) {
389+
// Restrict results to users already on this site.
390+
$include_blog_users = get_users(
391+
array(
392+
'blog_id' => $blog_id,
393+
'fields' => 'ID',
394+
)
395+
);
396+
}
397+
398+
// Include email in search columns only for users who can edit others.
399+
if ( current_user_can( 'edit_users' ) ) {
400+
$search_columns[] = 'user_email';
401+
}
402+
}
403+
404+
// Email tokens in the label should only be visible to users who can edit others.
405+
if ( ! current_user_can( 'edit_users' ) ) {
406+
$label_template = str_replace( '{{user_email}}', '', $label_template );
407+
}
341408

342409
$users = get_users(
343410
array(
344-
'blog_id' => false,
345-
'search' => '*' . $_REQUEST['term'] . '*',
411+
'blog_id' => $blog_id,
412+
'search' => '*' . $term . '*',
346413
'include' => $include_blog_users,
347414
'exclude' => $exclude_blog_users,
348-
'search_columns' => array( 'user_login', 'user_nicename', 'user_email' ),
415+
'search_columns' => $search_columns,
416+
'number' => 20,
349417
)
350418
);
351419

420+
$return = array();
421+
352422
foreach ( $users as $user ) {
423+
// Replace supported tokens in the label template.
424+
$label = $label_template;
425+
foreach ( array( 'user_login', 'user_email', 'display_name' ) as $token ) {
426+
$label = str_replace( '{{' . $token . '}}', $user->$token, $label );
427+
}
428+
$label = str_replace( '{{user_id}}', $user->ID, $label );
429+
430+
if ( '' === trim( $label ) ) {
431+
$label = '(' . $user->user_login . ')';
432+
}
433+
353434
$return[] = array(
354-
/* translators: 1: User login, 2: User email address. */
355-
'label' => sprintf( _x( '%1$s (%2$s)', 'user autocomplete result' ), $user->user_login, $user->user_email ),
356-
'value' => $user->$field,
435+
'label' => esc_html( $label ),
436+
'value' => ( 'ID' === $field ) ? $user->ID : $user->$field,
357437
);
358438
}
359439

360-
wp_die( wp_json_encode( $return ) );
440+
/**
441+
* Filters the user autocomplete results array.
442+
*
443+
* @since 7.1.0
444+
*
445+
* @param array $return Array of result objects with 'label' and 'value' keys.
446+
* @param string $term The sanitized search term.
447+
*/
448+
$return = apply_filters( 'autocomplete_user_results', $return, $term );
449+
450+
wp_send_json( $return );
361451
}
362452

363453
/**

src/wp-admin/users.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,12 @@
218218
wp_delete_user( $id );
219219
break;
220220
case 'reassign':
221-
wp_delete_user( $id, $_REQUEST['reassign_user'] );
221+
$reassign_id = isset( $_REQUEST['reassign_user'] ) ? absint( $_REQUEST['reassign_user'] ) : 0;
222+
if ( $reassign_id > 0 && $reassign_id !== $id ) {
223+
wp_delete_user( $id, $reassign_id );
224+
} else {
225+
wp_delete_user( $id );
226+
}
222227
break;
223228
}
224229

0 commit comments

Comments
 (0)