diff --git a/config/schema/decoupled_auth.schema.yml b/config/schema/decoupled_auth.schema.yml index 1b56b21..56d9444 100644 --- a/config/schema/decoupled_auth.schema.yml +++ b/config/schema/decoupled_auth.schema.yml @@ -25,3 +25,23 @@ decoupled_auth.settings: label: 'Selected roles' sequence: type: string + +entity_reference_selection.decoupled_auth_user: + type: entity_reference_selection + mapping: + filter: + type: mapping + label: 'Filter settings' + mapping: + type: + type: string + label: 'Filter by' + role: + type: sequence + label: 'Restrict to the selected roles' + sequence: + type: string + label: 'Role' + include_decoupled: + type: boolean + label: 'Include the decoupled users.' \ No newline at end of file diff --git a/modules/crm/decoupled_auth_crm.info.yml b/modules/crm/decoupled_auth_crm.info.yml index 799abcd..c5be3bf 100644 --- a/modules/crm/decoupled_auth_crm.info.yml +++ b/modules/crm/decoupled_auth_crm.info.yml @@ -7,6 +7,7 @@ core: 8.x dependencies: - decoupled_auth + - og - user - profile - address diff --git a/modules/crm/decoupled_auth_crm.install b/modules/crm/decoupled_auth_crm.install new file mode 100644 index 0000000..f800621 --- /dev/null +++ b/modules/crm/decoupled_auth_crm.install @@ -0,0 +1,41 @@ +addGroup('user', 'user'); + + // Add reference field to users. + $settings = ['field_name' => 'crm_organisation']; + $field = Drupal\og\Og::createField('og_group_ref', 'user', 'user', $settings); + + $field->setSetting('handler', 'decoupled_auth_user'); + $handler_settings = [ + 'include_decoupled' => 1, + 'filter' => [ + 'type' => 'role', + 'role' => [ + 'crm_org' => 'crm_org', + ] + ] + ]; + $field->setSetting('handler_settings', $handler_settings); + $field->save(); +} + +/** + * Implements hook_uninstall(). + */ +function decoupled_auth_crm_uninstall() { + // Remove og organisation field. + $field = Drupal\field\Entity\FieldConfig::load('user.user.crm_organisation'); + $field->delete(); +} \ No newline at end of file diff --git a/src/Plugin/EntityReferenceSelection/DecoupledUserSelection.php b/src/Plugin/EntityReferenceSelection/DecoupledUserSelection.php new file mode 100644 index 0000000..c4e14f7 --- /dev/null +++ b/src/Plugin/EntityReferenceSelection/DecoupledUserSelection.php @@ -0,0 +1,228 @@ +connection = $connection; + $this->userStorage = $entity_manager->getStorage('user'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager'), + $container->get('module_handler'), + $container->get('current_user'), + $container->get('database') + ); + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $selection_handler_settings = $this->configuration['handler_settings']; + + // Merge in default values. + $selection_handler_settings += array( + 'filter' => array( + 'type' => '_none', + ), + 'include_anonymous' => TRUE, + 'include_decoupled' => TRUE, + ); + + $form['include_decoupled'] = array( + '#type' => 'checkbox', + '#title' => $this->t('Include the decoupled users.'), + '#default_value' => $selection_handler_settings['include_decoupled'], + ); + + // Add user specific filter options. + $form['filter']['type'] = array( + '#type' => 'select', + '#title' => $this->t('Filter by'), + '#options' => array( + '_none' => $this->t('- None -'), + 'role' => $this->t('User role'), + ), + '#ajax' => TRUE, + '#limit_validation_errors' => array(), + '#default_value' => $selection_handler_settings['filter']['type'], + ); + + $form['filter']['settings'] = array( + '#type' => 'container', + '#attributes' => array('class' => array('entity_reference-settings')), + '#process' => array(array('\Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', 'formProcessMergeParent')), + ); + + if ($selection_handler_settings['filter']['type'] == 'role') { + // Merge in default values. + $selection_handler_settings['filter'] += array( + 'role' => NULL, + ); + + $form['filter']['settings']['role'] = array( + '#type' => 'checkboxes', + '#title' => $this->t('Restrict to the selected roles'), + '#required' => TRUE, + '#options' => array_diff_key(user_role_names(TRUE), array(RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID)), + '#default_value' => $selection_handler_settings['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); + + // The user entity doesn't have a label column. + if (isset($match)) { + $or = $query->orConditionGroup(); + $or->condition('name', $match, $match_operator); + $or->condition('mail', $match, $match_operator); + $query->condition($or); + } + + // Filter by role. + $handler_settings = $this->configuration['handler_settings']; + if (!empty($handler_settings['filter']['role'])) { + $query->condition('roles', $handler_settings['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 (!empty($this->configuration['handler_settings']['filter']['role'])) { + $entities = array_filter($entities, function ($user) { + /** @var \Drupal\user\UserInterface $user */ + return !empty(array_intersect($user->getRoles(), $this->configuration['handler_settings']['filter']['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) { + // Only show decoupled users if requested and user has permission. + $handler_settings = $this->configuration['handler_settings']; + if (isset($handler_settings['include_decoupled']) && $handler_settings['include_decoupled']) { + // @TODO Use a better permission that administer users when available. + if (!$this->currentUser->hasPermission('administer users')) { + $query->isNotNull('name'); + } + } + else { + $query->isNotNull('name'); + } + } + +} diff --git a/tests/src/Kernel/DecoupledUserSelectionTest.php b/tests/src/Kernel/DecoupledUserSelectionTest.php new file mode 100644 index 0000000..c9d8a11 --- /dev/null +++ b/tests/src/Kernel/DecoupledUserSelectionTest.php @@ -0,0 +1,271 @@ +installConfig(['decoupled_auth']); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + $this->userRole = $this->createRole([]); + $this->adminUser = $this->createUser([], NULL, TRUE); + $this->setSelectionHandler(); + } + + /** + * Updates the selection handler. + * + * @param bool|FALSE $filter + * Whether or not the handler should filter by user role. + */ + protected function setSelectionHandler($filter = FALSE) { + $options = [ + 'target_type' => 'user', + 'handler' => 'decoupled_auth_user', + 'handler_settings' => [ + 'include_decoupled' => 1, + ], + ]; + + if ($filter) { + $options['handler_settings']['filter'] = [ + 'type' => 'role', + 'role' => [ + $this->userRole => $this->userRole, + ] + ]; + } + + $this->selectionHandler = $this->container->get('plugin.manager.entity_reference_selection')->getInstance($options); + } + + /** + * Creates a range of users to test against. + */ + protected function createTestUsers() { + // Decoupled users (with and without user role). + $this->decoupledUserNoRole = $this->createDecoupledUser(); + + $this->decoupledUserRole = $this->createDecoupledUser(); + $this->decoupledUserRole->activate(); + $this->decoupledUserRole->addRole($this->userRole); + $this->decoupledUserRole->save(); + + // Coupled users (with and without user role). + $this->coupledUserNoRole = $this->createUser(); + + $this->coupledUserRole = $this->createUser(); + $this->coupledUserRole->addRole($this->userRole); + $this->coupledUserRole->save(); + } + + /** + * Sets the current account. + * + * @param \Drupal\Core\Session\AccountInterface $account + */ + protected function setCurrentAccount(AccountInterface $account) { + $this->container->get('account_switcher')->switchTo($account); + } + + /** + * Testing selection handler class. + */ + public function testSelectionHandlerClass() { + $this->assertEquals(get_class($this->selectionHandler), 'Drupal\decoupled_auth\Plugin\EntityReferenceSelection\DecoupledUserSelection'); + } + + + /** + * Testing selection handler results without role filter. + * + * All users should be referenceable without role filter. + * Decoupled users should only be referenceable for the admin user. + */ + public function testSelectionHandlerResults() { + // Create the demo users. + $this->createTestUsers(); + + // Get selection users. + $groups = $this->selectionHandler->getReferenceableEntities(); + $gids = array_keys($groups['user']); + + // Check results. + $this->assertTrue(in_array($this->coupledUserNoRole->id(), $gids)); + $this->assertTrue(in_array($this->coupledUserRole->id(), $gids)); + $this->assertFalse(in_array($this->decoupledUserNoRole->id(), $gids)); + $this->assertFalse(in_array($this->decoupledUserRole->id(), $gids)); + + // Switch to admin user. + $this->setCurrentAccount($this->adminUser); + + // Get selection users. + $groups = $this->selectionHandler->getReferenceableEntities(); + $gids = array_keys($groups['user']); + + // Check results. + $this->assertTrue(in_array($this->coupledUserNoRole->id(), $gids)); + $this->assertTrue(in_array($this->coupledUserRole->id(), $gids)); + $this->assertTrue(in_array($this->decoupledUserNoRole->id(), $gids)); + $this->assertTrue(in_array($this->decoupledUserRole->id(), $gids)); + } + + /** + * Testing selection handler results with role filter. + * + * Only user with the correct role should be referenceable. + * Decoupled users should only be referenceable for the admin user. + */ + public function testSelectionHandlerRolesResults() { + // Create the demo users and use the handler with role filter. + $this->setSelectionHandler(TRUE); + $this->createTestUsers(); + + // Get selection users. + $groups = $this->selectionHandler->getReferenceableEntities(); + $gids = array_keys($groups['user']); + + // Check results. + $this->assertFalse(in_array($this->coupledUserNoRole->id(), $gids)); + $this->assertTrue(in_array($this->coupledUserRole->id(), $gids)); + $this->assertFalse(in_array($this->decoupledUserNoRole->id(), $gids)); + $this->assertFalse(in_array($this->decoupledUserRole->id(), $gids)); + + // Switch to admin user. + $this->setCurrentAccount($this->adminUser); + + // Get selection users. + $groups = $this->selectionHandler->getReferenceableEntities(); + $gids = array_keys($groups['user']); + + // Check results. + $this->assertFalse(in_array($this->coupledUserNoRole->id(), $gids)); + $this->assertTrue(in_array($this->coupledUserRole->id(), $gids)); + $this->assertFalse(in_array($this->decoupledUserNoRole->id(), $gids)); + $this->assertTrue(in_array($this->decoupledUserRole->id(), $gids)); + } + + /** + * Testing selection handler results with matches. + * + * Check matching works on username and email for decoupled and coupled users. + */ + public function testSelectionHandlerMatchResults() { + // Create the demo users and use the handler with role filter. + $this->createTestUsers(); + $this->setCurrentAccount($this->adminUser); + + // Get selection users with email match. + $groups = $this->selectionHandler->getReferenceableEntities('@'); + $gids = array_keys($groups['user']); + + // Check results. + $this->assertTrue(in_array($this->coupledUserNoRole->id(), $gids)); + $this->assertTrue(in_array($this->coupledUserRole->id(), $gids)); + $this->assertTrue(in_array($this->decoupledUserNoRole->id(), $gids)); + $this->assertTrue(in_array($this->decoupledUserRole->id(), $gids)); + + + // Get selection users with username match. + $groups = $this->selectionHandler->getReferenceableEntities($this->coupledUserNoRole->getAccountName()); + $gids = array_keys($groups['user']); + + // Check results. + $this->assertTrue(in_array($this->coupledUserNoRole->id(), $gids)); + $this->assertFalse(in_array($this->coupledUserRole->id(), $gids)); + $this->assertFalse(in_array($this->decoupledUserNoRole->id(), $gids)); + $this->assertFalse(in_array($this->decoupledUserRole->id(), $gids)); + + // Get selection users with email prefix. + $groups = $this->selectionHandler->getReferenceableEntities($this->decoupledUserNoRole->email_prefix); + $gids = array_keys($groups['user']); + + // Check results. + $this->assertFalse(in_array($this->coupledUserNoRole->id(), $gids)); + $this->assertFalse(in_array($this->coupledUserRole->id(), $gids)); + $this->assertTrue(in_array($this->decoupledUserNoRole->id(), $gids)); + $this->assertFalse(in_array($this->decoupledUserRole->id(), $gids)); + } + +} diff --git a/travis-ci/travis-before-script.sh b/travis-ci/travis-before-script.sh index edd9584..ccfed1f 100644 --- a/travis-ci/travis-before-script.sh +++ b/travis-ci/travis-before-script.sh @@ -10,6 +10,7 @@ drupal_ti_ensure_drupal mkdir -p "$DRUPAL_TI_DRUPAL_DIR/$DRUPAL_TI_MODULES_PATH" cd "$DRUPAL_TI_DRUPAL_DIR/$DRUPAL_TI_MODULES_PATH" git clone --branch 8.x-1.x http://git.drupal.org/project/composer_manager.git --depth 1 +git clone --branch 8.x-1.x https://github.com/amitaibu/og.git --depth 1 php composer_manager/scripts/init.php # Ensure the module is linked into the codebase.