From 8aa3d01316ce2d04ba0bd599cc4af699e57033d6 Mon Sep 17 00:00:00 2001 From: Matthew Slater <matslats@fastmail.com> Date: Fri, 10 Apr 2020 08:39:40 +0200 Subject: [PATCH] improved the address plugin to understand 'firstname lastname' entity queries --- alt_login.module | 8 +- src/AltLoginMethodInterface.php | 11 +- src/Plugin/AltLoginMethod/AddressName.php | 39 ++- src/Plugin/AltLoginMethod/Email.php | 13 +- src/Plugin/AltLoginMethod/Uid.php | 9 +- src/Plugin/AltLoginMethod/Username.php | 11 +- .../AltLoginUserSelection.php | 36 ++- .../UserSelection.php | 258 ++++++++++++++++++ 8 files changed, 334 insertions(+), 51 deletions(-) create mode 100644 src/Plugin/EntityReferenceSelection/UserSelection.php diff --git a/alt_login.module b/alt_login.module index 2d7d5f1..6f49841 100644 --- a/alt_login.module +++ b/alt_login.module @@ -261,14 +261,8 @@ function alt_login_alt_login_info_alter(&$definitions) { unset($definitions['username']); $definitions['username'] = $def; } + //this should really be implemented using the plugin annotation if (!\Drupal::moduleHandler()->moduleExists('address')) { unset($definitions['address_name']); } } - -/** - * - */ -function alt_login_form_masquerade_block_form_alter(&$form, $form_state) { - $form['autocomplete']['masquerade_as']['#selection_handler'] = 'alt_login'; -} \ No newline at end of file diff --git a/src/AltLoginMethodInterface.php b/src/AltLoginMethodInterface.php index 1a2315d..8092958 100644 --- a/src/AltLoginMethodInterface.php +++ b/src/AltLoginMethodInterface.php @@ -3,8 +3,7 @@ namespace Drupal\alt_login; use Drupal\user\UserInterface; -use Drupal\Core\Entity\Query\QueryInterface; -use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Entity\Query\Sql\Condition; /** * Jnterface for AltLoginMethod plugins. @@ -52,11 +51,11 @@ interface AltLoginMethodInterface { /** - * Modify a user entityQuery + * Add conditions to an EntityQuery OR group * - * @param QueryInterface $query + * @param Condition $or_group * @param string $match - * @param string $match_operator + * @param Query $query */ - function entityQuery(Condition $or_group, $match, $match_operator); + function entityQuery(Condition $or_group, $match); } diff --git a/src/Plugin/AltLoginMethod/AddressName.php b/src/Plugin/AltLoginMethod/AddressName.php index f7e29ec..f8c8468 100644 --- a/src/Plugin/AltLoginMethod/AddressName.php +++ b/src/Plugin/AltLoginMethod/AddressName.php @@ -2,15 +2,16 @@ namespace Drupal\alt_login\Plugin\AltLoginMethod; -use Drupal\user\Entity\User; use Drupal\alt_login\AltLoginMethodInterface; use Drupal\user\UserInterface; -use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\user\Entity\User; use Drupal\Core\Database\Connection; use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\Query\Sql\Condition; +use Drupal\Core\Entity\Query\QueryFactory; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -43,13 +44,20 @@ class AddressName implements AltLoginMethodInterface, ContainerFactoryPluginInte private $entityFieldManager; /** - * @param EmailValidator $entity_field_manager + * @var QueryFactory + */ + private $entityQueryFactory; + + /** + * @param EntityFieldManagerInterface $entity_field_manager * @param Connection $database + * @param QueryFactory $query_factory * @param MessengerInterface $messenger */ - function __construct(EntityFieldManagerInterface $entity_field_manager, Connection $database, MessengerInterface $messenger) { + function __construct(EntityFieldManagerInterface $entity_field_manager, Connection $database, QueryFactory $query_factory, MessengerInterface $messenger) { $this->entityFieldManager = $entity_field_manager; $this->database = $database; + $this->entityQueryFactory = $query_factory; $this->messenger = $messenger; } @@ -65,6 +73,7 @@ class AddressName implements AltLoginMethodInterface, ContainerFactoryPluginInte return new static ( $container->get('entity_field.manager'), $container->get('database'), + $container->get('entity.query'), $container->get('messenger') ); } @@ -143,12 +152,22 @@ class AddressName implements AltLoginMethodInterface, ContainerFactoryPluginInte return $query->execute()->fetchCol(); } - function entityQuery(Condition $or_group, $match, $match_operator) { + /** + * {@inheritDoc} + */ + function entityQuery(Condition $or_group, $match) { $fname = $this->fieldName(); - //$table = 'user__'.$fname; - //$query->join('user__'.$fname, 'user__address', 'user_address.entity_id = u.entity_id'); - $or_group->condition($fname.'.given_name', $match, 'STARTS WITH'); - $or_group->condition($fname.'.family_name', $match, 'STARTS WITH'); + list($first, $last) = explode(' ', $match); + if (!$last) { + $or_group->condition($fname.'.given_name', $first, 'STARTS_WITH'); + $or_group->condition($fname.'.family_name', $first, 'STARTS_WITH'); + } + else { + $and_group = $this->entityQueryFactory->get('user')->andConditionGroup(); + $and_group->condition($fname.'.given_name', substr($first, 0, 4), 'STARTS_WITH'); + $and_group->condition($fname.'.family_name', $last, 'STARTS_WITH'); + $or_group->condition($and_group); + } } } diff --git a/src/Plugin/AltLoginMethod/Email.php b/src/Plugin/AltLoginMethod/Email.php index 638e41e..734d05f 100644 --- a/src/Plugin/AltLoginMethod/Email.php +++ b/src/Plugin/AltLoginMethod/Email.php @@ -6,9 +6,9 @@ use Drupal\alt_login\AltLoginMethodInterface; use Drupal\user\UserInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\Database\Query\Condition; -use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Entity\Query\Sql\Condition; use Drupal\Component\Utility\EmailValidatorInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation for logging in with the user name as an alias. @@ -79,10 +79,11 @@ class Email extends Username implements AltLoginMethodInterface, ContainerFactor return $user->getEmail(); } - function entityQuery(Condition $or_group, $match, $match_operator) { - if ($email_validator->isValid($match)) { - $or_group->condition('mail', $match, $match_operator); - } + /** + * {@inheritDoc} + */ + function entityQuery(Condition $or_group, $match) { + $or_group->condition('mail', $match, 'STARTS_WITH'); } } diff --git a/src/Plugin/AltLoginMethod/Uid.php b/src/Plugin/AltLoginMethod/Uid.php index 2e8bae7..a74e86b 100644 --- a/src/Plugin/AltLoginMethod/Uid.php +++ b/src/Plugin/AltLoginMethod/Uid.php @@ -5,7 +5,7 @@ namespace Drupal\alt_login\Plugin\AltLoginMethod; use Drupal\alt_login\AltLoginMethodInterface; use Drupal\user\UserInterface; use Drupal\user\Entity\User; -use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Entity\Query\Sql\Condition; /** * Plugin implementation for logging in with the user name as an alias. @@ -42,9 +42,12 @@ class Uid extends Username implements AltLoginMethodInterface { return $user->id(); } - function entityQuery(Condition $or_group, $match, $match_operator) { + /** + * {@inheritDoc} + */ + function entityQuery(Condition $or_group, $match) { if (is_numeric($match)) { - $or_group->condition('uid', $match, $match_operator); + $or_group->condition('uid', $match); } } } diff --git a/src/Plugin/AltLoginMethod/Username.php b/src/Plugin/AltLoginMethod/Username.php index b9e093e..b906794 100644 --- a/src/Plugin/AltLoginMethod/Username.php +++ b/src/Plugin/AltLoginMethod/Username.php @@ -4,7 +4,7 @@ namespace Drupal\alt_login\Plugin\AltLoginMethod; use Drupal\alt_login\AltLoginMethodInterface; use Drupal\user\UserInterface; -use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Entity\Query\Sql\Condition; /** * Plugin implementation for logging in with the user name as an alias. @@ -33,7 +33,6 @@ class Username implements AltLoginMethodInterface { return TRUE; } - /** * {@inheritDoc} */ @@ -41,7 +40,6 @@ class Username implements AltLoginMethodInterface { return $alias; } - /** * {@inheritDoc} */ @@ -49,8 +47,11 @@ class Username implements AltLoginMethodInterface { return $user->getUsername(); } - function entityQuery(Condition $or_group, $match, $match_operator) { - // if this plugin were NOT in use, it would be necesary to remove a part of the entity query + /** + * {@inheritDoc} + */ + function entityQuery(Condition $or_group, $match) { + $or_group->condition('name', $match, 'STARTS_WITH'); } } diff --git a/src/Plugin/EntityReferenceSelection/AltLoginUserSelection.php b/src/Plugin/EntityReferenceSelection/AltLoginUserSelection.php index 2b174d2..97a7128 100644 --- a/src/Plugin/EntityReferenceSelection/AltLoginUserSelection.php +++ b/src/Plugin/EntityReferenceSelection/AltLoginUserSelection.php @@ -4,7 +4,6 @@ namespace Drupal\alt_login\Plugin\EntityReferenceSelection; use Drupal\Core\Database\Query\SelectInterface; use Drupal\user\Plugin\EntityReferenceSelection\UserSelection; -use Drupal\Core\Database\Query\Condition; /** * Provides specific access control for the user entity type. @@ -21,25 +20,34 @@ class AltLoginUserSelection extends UserSelection { /** * {@inheritdoc} + * + * @note This doesn't inherit from its ancestors because we're not presuming + * to search on the user name. */ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { - $query = parent::buildEntityQuery($match, $match_operator); - $or = new Condition('OR'); + $query = $this->entityTypeManager->getStorage('user')->getQuery(); + $query->addTag('user_access'); + $query->addTag('entity_reference'); + $query->addMetaData('entity_reference_selection_handler', $this); + + $configuration = $this->getConfiguration(); + // Add the sort option. + if ($configuration['sort']['field'] !== '_none') { + $query->sort($configuration['sort']['field'], $configuration['sort']['direction']); + } + if (!$configuration['include_anonymous']) { + $query->condition('uid', 0, '<>'); + } + if (!empty($configuration['filter']['role'])) { + $query->condition('roles', $configuration['filter']['role'], 'IN'); + } + $or = $query->orConditionGroup(); foreach (alt_login_active_plugins() as $plugin_id => $plugin) { - $plugin->entityQuery($or, $match, $match_operator); + $plugin->entityQuery($or, $match, $query); } - $query->orConditionGroup($or); + $query->condition($or); return $query; } - - /** - * {@inheritdoc} - */ - public function entityQueryAlter(SelectInterface $query) { - parent::entityQueryAlter($query); - - } - } diff --git a/src/Plugin/EntityReferenceSelection/UserSelection.php b/src/Plugin/EntityReferenceSelection/UserSelection.php new file mode 100644 index 0000000..e606c5a --- /dev/null +++ b/src/Plugin/EntityReferenceSelection/UserSelection.php @@ -0,0 +1,258 @@ +<?php + +namespace Drupal\user\Plugin\EntityReferenceSelection; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Query\Condition; +use Drupal\Core\Database\Query\SelectInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityRepositoryInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\user\RoleInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides specific access control for the user entity type. + * + * @EntityReferenceSelection( + * id = "default:user", + * label = @Translation("User selection"), + * entity_types = {"user"}, + * group = "default", + * weight = 1 + * ) + */ +class UserSelection extends DefaultSelection { + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * Constructs a new UserSelection object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager service. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler service. + * @param \Drupal\Core\Session\AccountInterface $current_user + * The current user. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager + * The entity field manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The entity type bundle info service. + * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository + * The entity repository. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, AccountInterface $current_user, Connection $connection, EntityFieldManagerInterface $entity_field_manager = NULL, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, EntityRepositoryInterface $entity_repository = NULL) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $module_handler, $current_user, $entity_field_manager, $entity_type_bundle_info, $entity_repository); + + $this->connection = $connection; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('module_handler'), + $container->get('current_user'), + $container->get('database'), + $container->get('entity_field.manager'), + $container->get('entity_type.bundle.info'), + $container->get('entity.repository') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'filter' => [ + 'type' => '_none', + 'role' => NULL, + ], + 'include_anonymous' => TRUE, + ] + parent::defaultConfiguration(); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $configuration = $this->getConfiguration(); + + $form['include_anonymous'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Include the anonymous user.'), + '#default_value' => $configuration['include_anonymous'], + ]; + + // Add user specific filter options. + $form['filter']['type'] = [ + '#type' => 'select', + '#title' => $this->t('Filter by'), + '#options' => [ + '_none' => $this->t('- None -'), + 'role' => $this->t('User role'), + ], + '#ajax' => TRUE, + '#limit_validation_errors' => [], + '#default_value' => $configuration['filter']['type'], + ]; + + $form['filter']['settings'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['entity_reference-settings']], + '#process' => [['\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', 'formProcessMergeParent']], + ]; + + if ($configuration['filter']['type'] == 'role') { + $form['filter']['settings']['role'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Restrict to the selected roles'), + '#required' => TRUE, + '#options' => array_diff_key(user_role_names(TRUE), [RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID]), + '#default_value' => $configuration['filter']['role'], + ]; + } + + $form += parent::buildConfigurationForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { + $query = parent::buildEntityQuery($match, $match_operator); + + $configuration = $this->getConfiguration(); + + // Filter out the Anonymous user if the selection handler is configured to + // exclude it. + if (!$configuration['include_anonymous']) { + $query->condition('uid', 0, '<>'); + } + + // The user entity doesn't have a label column. + if (isset($match)) { + $query->condition('name', $match, $match_operator); + } + + // Filter by role. + if (!empty($configuration['filter']['role'])) { + $query->condition('roles', $configuration['filter']['role'], 'IN'); + } + + // Adding the permission check is sadly insufficient for users: core + // requires us to also know about the concept of 'blocked' and 'active'. + if (!$this->currentUser->hasPermission('administer users')) { + $query->condition('status', 1); + } + return $query; + } + + /** + * {@inheritdoc} + */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $user = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable user, it needs to be active. + if (!$this->currentUser->hasPermission('administer users')) { + /** @var \Drupal\user\UserInterface $user */ + $user->activate(); + } + + return $user; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + // Mirror the conditions checked in buildEntityQuery(). + if ($role = $this->getConfiguration()['filter']['role']) { + $entities = array_filter($entities, function ($user) use ($role) { + /** @var \Drupal\user\UserInterface $user */ + return !empty(array_intersect($user->getRoles(), $role)); + }); + } + if (!$this->currentUser->hasPermission('administer users')) { + $entities = array_filter($entities, function ($user) { + /** @var \Drupal\user\UserInterface $user */ + return $user->isActive(); + }); + } + return $entities; + } + + /** + * {@inheritdoc} + */ + public function entityQueryAlter(SelectInterface $query) { + parent::entityQueryAlter($query); + + // Bail out early if we do not need to match the Anonymous user. + if (!$this->getConfiguration()['include_anonymous']) { + return; + } + + if ($this->currentUser->hasPermission('administer users')) { + // In addition, if the user is administrator, we need to make sure to + // match the anonymous user, that doesn't actually have a name in the + // database. + $conditions = &$query->conditions(); + foreach ($conditions as $key => $condition) { + if ($key !== '#conjunction' && is_string($condition['field']) && $condition['field'] === 'users_field_data.name') { + // Remove the condition. + unset($conditions[$key]); + + // Re-add the condition and a condition on uid = 0 so that we end up + // with a query in the form: + // WHERE (name LIKE :name) OR (:anonymous_name LIKE :name AND uid = 0) + $or = new Condition('OR'); + $or->condition($condition['field'], $condition['value'], $condition['operator']); + // Sadly, the Database layer doesn't allow us to build a condition + // in the form ':placeholder = :placeholder2', because the 'field' + // part of a condition is always escaped. + // As a (cheap) workaround, we separately build a condition with no + // field, and concatenate the field and the condition separately. + $value_part = new Condition('AND'); + $value_part->condition('anonymous_name', $condition['value'], $condition['operator']); + $value_part->compile($this->connection, $query); + $or->condition((new Condition('AND')) + ->where(str_replace($query->escapeField('anonymous_name'), ':anonymous_name', (string) $value_part), $value_part->arguments() + [':anonymous_name' => \Drupal::config('user.settings')->get('anonymous')]) + ->condition('base_table.uid', 0) + ); + $query->condition($or); + } + } + } + } + +} -- GitLab