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