foldershare-8.x-1.2/src/Controller/UserAutocompleteController.php
src/Controller/UserAutocompleteController.php
<?php
namespace Drupal\foldershare\Controller;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Drupal\foldershare\Settings;
use Drupal\foldershare\Utilities\UserUtilities;
/**
* Provides a user name autocomplete service.
*
* The user name autocomplete service is intended for use by forms that
* require the user to type in a user or account name in order to
* select a user. There are two primary forms that may use this:
* - The change owner form that prompts for the account of the new owner
* of a selected item.
* - The share form that prompts for the account of a user to be granted
* shared access to a file or folder.
*
* This autocomplete service is available on the route
* "entity.foldershare.userautocomplete", defined in the module's routing file.
*
* Required URL arguments include:
* - 'q=NAMEFRAGMENT' to provide a string to look up as part of a user's
* account name, display name, or email address.
*
* Optional URL arguments include:
* - 'excludeUids=UIDLIST' to provide a comma-separated list of integer user IDs
* to NOT include in the returned autocomplete results. This may be used to
* exclude users in a 'Share' form that have already been granted access.
* - 'excludeBlocked=1' to to prevent the returned list from including blocked
* users.
*
* The returned user list always excludes users with IDs in the 'excludeUids'
* list, if any.
*
* The return list optionally excludes blocked users, if 'excludeBlocked' is 1.
*
* The returned user list is empty if:
* - User autocomplete has been disabled for the module.
* - The name fragment is empty.
* - The name fragment does not match anything.
*
* The returned list is an array, formatted as JSON, sorted by user name.
* Each entry is an array with 'value' and 'label' fields. The 'value' field
* is the user account name, and the 'label' a label to show in an
* autocomplete menu. Labels have one of these forms:
* - Name only.
* - Name with email address.
* - Name with masked email address.
*
* The label style is determined by a module setting.
*
* The name is the account display name, and the email address the account's
* email address (if any). When the masked form is used, the email address
* is masked with '*'s for all name field characters, except the first and
* last (e.g. "e*****e@example.com").
*
* @ingroup foldershare
*/
class UserAutocompleteController extends ControllerBase {
/*---------------------------------------------------------------------
*
* Constants.
*
*---------------------------------------------------------------------*/
/**
* The default maximum number of results to return.
*
* @var int
*/
const MAXIMUM_NUMBER_OF_RESULTS = 10;
/*--------------------------------------------------------------------
*
* Fields - dependency injection.
*
*--------------------------------------------------------------------*/
/**
* The User entity storage manager, set at construction time.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $userStorage;
/*--------------------------------------------------------------------
*
* Construction.
*
*--------------------------------------------------------------------*/
/**
* Constructs a new page.
*/
public function __construct(
EntityStorageInterface $userStorage) {
$this->userStorage = $userStorage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager')->getStorage('user'));
}
/*---------------------------------------------------------------------
*
* Autocomplete.
*
*---------------------------------------------------------------------*/
/**
* Responds to a text field's autocomplete request using a user name fragment.
*
* Auto-complete uses the given user name fragment to find all user names
* that are similar to the name. If configured using the FolderShare admin
* settings form, auto-complete may also look at email addresses to find
* a good match.
*
* The returned list is intended for the 'Share' form to indicate users
* with which to share content. There may already be users listed in the
* form, and returning them in the auto-complete would be redundant. To
* skip those users, an optional excludeUids list omits them from the returned
* results.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request, including two parameters:
* - 'q' has the text field input to auto-complete.
* - 'excludeUids' has a list of integer UIDs to exclude.
* - 'excludeBlocked' is 0 (FALSE) or 1 (TRUE) to indicate whether blocked
* user accounts should be excluded.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* The JSON response that contains an array of autocomplete content. Each
* entry in the JSON array is for a user. Each user entry is itself an
* array with 'value' and 'label' string values. The label is intended to
* be shown in an autocomplete menu, and the value is the value for that
* label.
*/
public function autocomplete(Request $request) {
//
// Get parameters.
// ---------------
// 'q' is the query input from the text field. It must not be empty.
// 'excludeUids' is the optional list of user IDs to skip in the returned
// results. It can be missing or empty.
$userNameFragment = $request->get('q', NULL);
$excludeUids = $request->get('excludeUids', []);
$excludeBlocked = $request->get('excludeBlocked', FALSE);
$excludeBlocked = boolval($excludeBlocked);
if (empty($userNameFragment) === TRUE) {
// No user name fragment given. Return an empty response.
return new JsonResponse([]);
}
//
// Get auto-complete style.
// ------------------------
// The following styles are recognized:
// - 'none'.
// - 'name-only'.
// - 'name-email'.
// - 'name-masked-email'.
$autocompleteStyle = Settings::getUserAutocompleteStyle();
if ($autocompleteStyle === 'none') {
// Auto-complete disabled. Return an empty response.
return new JsonResponse([]);
}
$matchEmail = FALSE;
if ($autocompleteStyle === 'name-email' ||
$autocompleteStyle === 'name-masked-email') {
$matchEmail = TRUE;
}
//
// Get a list of similar users.
// ----------------------------
// Match the given name fragment against user account names and user
// display names, if possible. Optionally match against user email
// addresses. Optionally exclude the given list of user IDs and
// blocked users.
$uids = UserUtilities::findSimilarUsers(
$userNameFragment,
$matchEmail,
$excludeBlocked,
$excludeUids,
self::MAXIMUM_NUMBER_OF_RESULTS);
if (empty($uids) === TRUE) {
// No match.
return new JsonResponse([]);
}
//
// Build returned JSON.
// --------------------
// Loop through the returned UIDs. For each one, we need:
// - the display name.
// - the email address (for appropriate auto-complete styles).
//
// If needed, the email address must be masked.
$results = [];
foreach ($uids as $uid) {
// Load the user.
$user = $this->userStorage->load($uid);
if ($user === NULL) {
// Invalid user.
continue;
}
// Get the user's display name. If there is no full name, this falls
// back to the account name.
$userDisplayName = $user->getDisplayName();
// Get the user's email address, if needed. Mask it, if needed.
if ($autocompleteStyle === 'name-email' ||
$autocompleteStyle === 'name-masked-email') {
$userEmail = $user->getEmail();
if ($autocompleteStyle === 'name-masked-email' &&
empty($userEmail) === FALSE) {
$userEmail = $this->maskEmail($userEmail);
}
if (empty($userEmail) === TRUE) {
// The account has no email address, or it is malformed.
// Skip it.
$label = $this->t(
'@userDisplayName',
[
'@userDisplayName' => $userDisplayName,
]);
}
else {
$label = $this->t(
'@userDisplayName (@userEmail)',
[
'@userDisplayName' => $userDisplayName,
'@userEmail' => $userEmail,
]);
}
}
else {
$label = $this->t(
'@userDisplayName',
[
'@userDisplayName' => $userDisplayName,
]);
}
$results[] = [
'value' => $user->getAccountName(),
'label' => $label,
];
unset($user);
}
return new JsonResponse($results);
}
/**
* Returns a masked version of a given email address.
*
* Masking keeps the first and last characters of the name portion of
* the email address, and all of the rest of the email address. The
* intervening letters are replaced with '*'.
*
* @param string $email
* The email address to mask.
*
* @return string
* Returns the masked email address.
*/
private function maskEmail(string $email) {
if (empty($email) === TRUE) {
// Missing email address.
return '';
}
// Email addresses are UTF-8 strings that may include non-ASCII characters
// in both the name and domain parts of the address. Proper string handling
// then requires that we use the PHP mb_* functions for multi-byte strings.
//
// Split the address into name and domain parts.
$parts = mb_split('@', $email);
if (count($parts) !== 2) {
// Malformed email address either has no '@' or too many.
// Masking is not defined in this case, so return nothing.
return '';
}
// Create a masked name with the original first and last characters,
// and the rest replaced with '*'.
$masked = mb_substr($email, 0, 1);
$len = mb_strlen($parts[0]) - 2;
for ($i = 0; $i < $len; $i++) {
$masked .= '*';
}
$masked .= mb_substr($email, $len + 1, 1);
return $masked . '@' . $parts[1];
}
}
