diff --git a/modules/ggroup/src/Access/SubgroupAddAccessCheck.php b/modules/ggroup/src/Access/SubgroupAddAccessCheck.php index 2e251ff..fbeb93d 100644 --- a/modules/ggroup/src/Access/SubgroupAddAccessCheck.php +++ b/modules/ggroup/src/Access/SubgroupAddAccessCheck.php @@ -23,7 +23,7 @@ class SubgroupAddAccessCheck implements AccessInterface { * The currently logged in account. * @param \Drupal\group\Entity\GroupInterface $group * The group to create the subgroup in. - * @param \Drupal\group\GroupTypeInterface $group_type + * @param \Drupal\group\Entity\GroupTypeInterface $group_type * The type of subgroup to create in the group. * * @return \Drupal\Core\Access\AccessResultInterface diff --git a/modules/ggroup/src/Controller/SubgroupWizardController.php b/modules/ggroup/src/Controller/SubgroupWizardController.php index 1a4880b..13050ae 100644 --- a/modules/ggroup/src/Controller/SubgroupWizardController.php +++ b/modules/ggroup/src/Controller/SubgroupWizardController.php @@ -152,8 +152,9 @@ class SubgroupWizardController extends ControllerBase { * @param \Drupal\group\Entity\GroupInterface $group * The group to add the subgroup to. * - * @return array - * The subgroup creation overview page. + * @return array|\Symfony\Component\HttpFoundation\RedirectResponse + * The subgroup creation overview page or a redirect to the create form if + * we only have 1 bundle. */ public function addPage(GroupInterface $group) { // We do not set the "entity_add_list" template's "#add_bundle_message" key diff --git a/modules/ggroup/src/Graph/CyclicGraphException.php b/modules/ggroup/src/Graph/CyclicGraphException.php index c595a04..690e710 100644 --- a/modules/ggroup/src/Graph/CyclicGraphException.php +++ b/modules/ggroup/src/Graph/CyclicGraphException.php @@ -3,8 +3,7 @@ namespace Drupal\ggroup\Graph; /** - * An exception thrown when a potential cycle is detected within an acyclic - * graph. + * An exception thrown when a potential cycle is detected in an acyclic graph. */ class CyclicGraphException extends \Exception { @@ -15,11 +14,9 @@ class CyclicGraphException extends \Exception { * The parent group ID or name. * @param int|string $child * The child group ID or name. - * @return static - * A new class instance. */ public function __construct($parent, $child) { parent::__construct("Cannot add group '$child' as a subgroup of group '$parent' because group '$parent' is already a descendant subgroup of group '$child'. Cyclic relationships cannot be handled."); } -} \ No newline at end of file +} diff --git a/modules/ggroup/src/Graph/GroupGraphStorageInterface.php b/modules/ggroup/src/Graph/GroupGraphStorageInterface.php index 6687319..b72d961 100644 --- a/modules/ggroup/src/Graph/GroupGraphStorageInterface.php +++ b/modules/ggroup/src/Graph/GroupGraphStorageInterface.php @@ -8,37 +8,42 @@ namespace Drupal\ggroup\Graph; interface GroupGraphStorageInterface { /** - * Relates group A and group B such that group B will be a child of group A. - * Inferred relationships based on existing relationships to group A and - * group B will also be created. + * Relates the parent group and the child group. * - * @param int $a + * Inferred relationships based on existing relationships to the parent group + * and the child group will also be created. + * + * @param int $parent_group_id * The ID of the parent group. - * @param int $b + * @param int $child_group_id * The ID of the child group. - * @return int|FALSE - * The ID of the graph edge relating parent group A to child group B or + * + * @return int|false + * The ID of the graph edge relating the parent group to the child group or * FALSE if the relationship could not be created. */ - public function addEdge($a, $b); + public function addEdge($parent_group_id, $child_group_id); /** - * Removes the relationship between group A and group B. Group B will no - * longer be a child of group A. Inferred relationships based on existing - * relationships to group A and group B will also be removed. + * Removes the relationship between the parent group and the child group. * - * @param int $a + * The child group will no longer be a child of the parent group. Inferred + * relationships based on existing relationships to the parent group will + * also be removed. + * + * @param int $parent_group_id * The ID of the parent group. - * @param int $b + * @param int $child_group_id * The ID of the child group. */ - public function removeEdge($a, $b); + public function removeEdge($parent_group_id, $child_group_id); /** * Gets all descendant child groups of the given parent group. * * @param int $group_id * The parent group ID. + * * @return int[] * An array of descendant child group IDs. */ @@ -49,6 +54,7 @@ interface GroupGraphStorageInterface { * * @param int $group_id * The child group ID. + * * @return int[] * An array of ancestor parent group IDs. */ @@ -61,6 +67,7 @@ interface GroupGraphStorageInterface { * The group whose ancestry status will be checked. * @param int $b * The group for which ancestry status will be checked against. + * * @return bool * TRUE if group A is an ancestor of group B. */ @@ -73,9 +80,26 @@ interface GroupGraphStorageInterface { * The group whose descent status will be checked. * @param int $b * The group for which descent status will be checked against. + * * @return bool * TRUE if group A is an descendant of group B. */ public function isDescendant($a, $b); -} \ No newline at end of file + /** + * Use the Breadth-first search algoritm to find the path between groups. + * + * @param int $parent_group_id + * The ID of the parent group. + * @param int $child_group_id + * The ID of the child group. + * + * @return array[] + * An nested array containing a path between the groups. + * + * @see https://en.wikipedia.org/wiki/Breadth-first_search + * @see https://www.sitepoint.com/data-structures-4/ + */ + public function getPath($parent_group_id, $child_group_id); + +} diff --git a/modules/ggroup/src/Graph/SqlGroupGraphStorage.php b/modules/ggroup/src/Graph/SqlGroupGraphStorage.php index e57419e..397f5a4 100644 --- a/modules/ggroup/src/Graph/SqlGroupGraphStorage.php +++ b/modules/ggroup/src/Graph/SqlGroupGraphStorage.php @@ -3,9 +3,7 @@ namespace Drupal\ggroup\Graph; use Drupal\Core\Database\Connection; -use Drupal\Core\Database\Database; -use Drupal\ggroup\Graph\GroupGraphStorageInterface; -use Drupal\ggroup\Graph\CyclicGraphException; +use Drupal\Core\Database\Query\Condition; /** * SQL based storage of the group relationship graph. @@ -24,51 +22,51 @@ class SqlGroupGraphStorage implements GroupGraphStorageInterface { * * @param \Drupal\Core\Database\Connection $connection * The database connection. - * @return static - * A new class instance. */ public function __construct(Connection $connection) { $this->connection = $connection; } /** - * Gets the edge ID relating parent group A to child group B. + * Gets the edge ID relating the parent group to the child group. * - * @param int $a + * @param int $parent_group_id * The ID of the parent group. - * @param int $b + * @param int $child_group_id * The ID of the child group. + * * @return int - * The ID of the edge relating parent group A to child group B. + * The ID of the edge relating the parent group to the child group. */ - protected function getEdgeId($a, $b) { - return $this->connection->query('SELECT gg.id FROM {group_graph} gg WHERE - gg.start_vertex = :a AND - gg.end_vertex = :b AND - gg.hops = 0', [ - ':a' => $a, - ':b' => $b, - ])->fetchField(); + protected function getEdgeId($parent_group_id, $child_group_id) { + $query = $this->connection->select('group_graph', 'gg') + ->fields('gg', ['id']); + $query->condition('start_vertex', $parent_group_id); + $query->condition('end_vertex', $child_group_id); + $query->condition('hops', 0); + return $query->execute()->fetchField(); } /** - * Relates parent group A to child group B so that child group B can be - * considered a subgroup of group A. This method only creates the relationship - * from group A to group B and not any of the inferred relationships based on - * what other relationships group A and group B already have. + * Relates the parent group to the child group. + * + * This method only creates the relationship from the parent group to the + * child group and not any of the inferred relationships based on what other + * relationships the parent group and the child group already have. * - * @param int $a + * @param int $parent_group_id * The ID of the parent group. - * @param int $b + * @param int $child_group_id * The ID of the child group. + * * @return int - * The ID of the new edge relating parent group A to child group B. + * The ID of the new edge relating the parent group to the child group. */ - protected function insertEdge($a, $b) { + protected function insertEdge($parent_group_id, $child_group_id) { $new_edge_id = $this->connection->insert('group_graph') ->fields([ - 'start_vertex' => $a, - 'end_vertex' => $b, + 'start_vertex' => $parent_group_id, + 'end_vertex' => $child_group_id, 'hops' => 0, ]) ->execute(); @@ -86,147 +84,140 @@ class SqlGroupGraphStorage implements GroupGraphStorageInterface { } /** - * @todo Add description. + * Insert parent group incoming edges to child group. * * @param int $edge_id - * The existing edge ID relating parent group A to child group B. - * @param int $a + * The existing edge ID relating the parent group to the child group. + * @param int $parent_group_id * The ID of the parent group. - * @param int $b + * @param int $child_group_id * The ID of the child group. */ - protected function insertEdgesAIncomingToB($edge_id, $a, $b) { - // A's incoming edges to B. - $insert_query = <<connection->query($insert_query, [ - ':edge_id' => $edge_id, - ':a' => $a, - ':b' => $b, - ]); + protected function insertEdgesParentIncomingToChild($edge_id, $parent_group_id, $child_group_id) { + // Since fields are added before expressions, all fields are added as + // expressions to keep the field order intact. + $query = $this->connection->select('group_graph', 'gg'); + $query->addExpression('gg.id', 'entry_edge_id'); + $query->addExpression($edge_id, 'direct_edge_id'); + $query->addExpression($edge_id, 'exit_edge_id'); + $query->addExpression('gg.start_vertex', 'start_vertex'); + $query->addExpression($child_group_id, 'end_vertex'); + $query->addExpression('gg.hops + 1', 'hops'); + $query->condition('end_vertex', $parent_group_id); + + $this->connection->insert('group_graph') + ->fields([ + 'entry_edge_id', + 'direct_edge_id', + 'exit_edge_id', + 'start_vertex', + 'end_vertex', + 'hops', + ]) + ->from($query) + ->execute(); } /** - * @todo Add description. + * Insert parent group outgoing edges to child group. * * @param int $edge_id - * The existing edge ID relating parent group A to child group B. - * @param int $a + * The existing edge ID relating the parent group to the child group. + * @param int $parent_group_id * The ID of the parent group. - * @param int $b + * @param int $child_group_id * The ID of the child group. */ - protected function insertEdgesAToBOutgoing($edge_id, $a, $b) { - // A to B's outgoing edges. - $insert_query = <<connection->query($insert_query, [ - ':edge_id' => $edge_id, - ':a' => $a, - ':b' => $b, - ]); + protected function insertEdgesParentToChildOutgoing($edge_id, $parent_group_id, $child_group_id) { + // Since fields are added before expressions, all fields are added as + // expressions to keep the field order intact. + $query = $this->connection->select('group_graph', 'gg'); + $query->addExpression($edge_id, 'entry_edge_id'); + $query->addExpression($edge_id, 'direct_edge_id'); + $query->addExpression('gg.id', 'exit_edge_id'); + $query->addExpression($parent_group_id, 'start_vertex'); + $query->addExpression('gg.end_vertex', 'end_vertex'); + $query->addExpression('gg.hops + 1', 'hops'); + $query->condition('start_vertex', $child_group_id); + + $this->connection->insert('group_graph') + ->fields([ + 'entry_edge_id', + 'direct_edge_id', + 'exit_edge_id', + 'start_vertex', + 'end_vertex', + 'hops', + ]) + ->from($query) + ->execute(); } /** - * @todo Add description. + * Insert the parent group incoming edges to the child group outgoing edges. * * @param int $edge_id - * The existing edge ID relating parent group A to child group B. - * @param int $a + * The existing edge ID relating the parent group to the child group. + * @param int $parent_group_id * The ID of the parent group. - * @param int $b + * @param int $child_group_id * The ID of the child group. */ - protected function insertEdgesAIncomingToBOutgoing($edge_id, $a, $b) { - // A’s incoming edges to B's outgoing edges. - $insert_query = <<connection->query($insert_query, [ - ':edge_id' => $edge_id, - ':a' => $a, - ':b' => $b, - ]); - + protected function insertEdgesParentIncomingToChildOutgoing($edge_id, $parent_group_id, $child_group_id) { + // Since fields are added before expressions, all fields are added as + // expressions to keep the field order intact. + $query = $this->connection->select('group_graph', 'parent_gg'); + $query->join('group_graph', 'child_gg'); + $query->addExpression('parent_gg.id', 'entry_edge_id'); + $query->addExpression($edge_id, 'direct_edge_id'); + $query->addExpression('child_gg.id', 'exit_edge_id'); + $query->addExpression('parent_gg.start_vertex', 'start_vertex'); + $query->addExpression('child_gg.end_vertex', 'end_vertex'); + $query->addExpression('parent_gg.hops + child_gg.hops + 1', 'hops'); + $query->condition('parent_gg.end_vertex', $parent_group_id); + $query->condition('child_gg.start_vertex', $child_group_id); + + $this->connection->insert('group_graph') + ->fields([ + 'entry_edge_id', + 'direct_edge_id', + 'exit_edge_id', + 'start_vertex', + 'end_vertex', + 'hops', + ]) + ->from($query) + ->execute(); } /** * {@inheritdoc} */ - public function addEdge($a, $b) { - if ($a === $b) { + public function addEdge($parent_group_id, $child_group_id) { + if ($parent_group_id === $child_group_id) { return FALSE; } - $ab_edge_id = $this->getEdgeId($a, $b); + $parent_child_edge_id = $this->getEdgeId($parent_group_id, $child_group_id); - if (!empty($ab_edge_id)) { - return $ab_edge_id; + if (!empty($parent_child_edge_id)) { + return $parent_child_edge_id; } - $ba_edge_id = $this->getEdgeId($a, $b); + $child_parent_edge_id = $this->getEdgeId($parent_group_id, $child_group_id); - if (!empty($ba_edge_id)) { - return $ba_edge_id; + if (!empty($child_parent_edge_id)) { + return $child_parent_edge_id; } - if ($this->isDescendant($a, $b)) { - throw new CyclicGraphException($a, $b); + if ($this->isDescendant($parent_group_id, $child_group_id)) { + throw new CyclicGraphException($parent_group_id, $child_group_id); } - $new_edge_id = $this->insertEdge($a, $b); - $this->insertEdgesAIncomingToB($new_edge_id, $a, $b); - $this->insertEdgesAToBOutgoing($new_edge_id, $a, $b); - $this->insertEdgesAIncomingToBOutgoing($new_edge_id, $a, $b); + $new_edge_id = $this->insertEdge($parent_group_id, $child_group_id); + $this->insertEdgesParentIncomingToChild($new_edge_id, $parent_group_id, $child_group_id); + $this->insertEdgesParentToChildOutgoing($new_edge_id, $parent_group_id, $child_group_id); + $this->insertEdgesParentIncomingToChildOutgoing($new_edge_id, $parent_group_id, $child_group_id); return $new_edge_id; } @@ -234,8 +225,8 @@ EOT; /** * {@inheritdoc} */ - public function removeEdge($a, $b) { - $edge_id = $this->getEdgeId($a, $b); + public function removeEdge($parent_group_id, $child_group_id) { + $edge_id = $this->getEdgeId($parent_group_id, $child_group_id); if (empty($edge_id)) { return; @@ -243,9 +234,10 @@ EOT; $edges_to_delete = []; - $results = $this->connection->query('SELECT gg.id FROM {group_graph} gg WHERE direct_edge_id = :edge_id', [ - ':edge_id' => $edge_id - ]); + $query = $this->connection->select('group_graph', 'gg') + ->fields('gg', ['id']); + $query->condition('direct_edge_id', $edge_id); + $results = $query->execute(); while ($id = $results->fetchField()) { $edges_to_delete[] = $id; @@ -255,18 +247,18 @@ EOT; return; } - $select_query = << 0 AND - (entry_edge_id IN (:edge_ids[]) OR exit_edge_id IN (:edge_ids[])) AND - (id NOT IN (:edge_ids[])) -EOT; - do { $total_edges = count($edges_to_delete); - $results = $this->connection->query($select_query, [ - ':edge_ids[]' => $edges_to_delete, - ]); + $query = $this->connection->select('group_graph', 'gg') + ->fields('gg', ['id']); + $query->condition('hops', 0); + $query->condition('id', $edges_to_delete, 'NOT IN'); + $query_or_conditions = new Condition('OR'); + $query_or_conditions->condition('entry_edge_id', $edges_to_delete, 'IN'); + $query_or_conditions->condition('exit_edge_id', $edges_to_delete, 'IN'); + $query->condition($query_or_conditions); + $results = $query->execute(); while ($id = $results->fetchField()) { $edges_to_delete[] = $id; @@ -282,18 +274,20 @@ EOT; * {@inheritdoc} */ public function getDescendants($group_id) { - return $this->connection->query('SELECT end_vertex FROM {group_graph} WHERE start_vertex = :group_id', [ - ':group_id' => $group_id, - ])->fetchAll(\PDO::FETCH_COLUMN); + $query = $this->connection->select('group_graph', 'gg') + ->fields('gg', ['end_vertex']); + $query->condition('start_vertex', $group_id); + return $query->execute()->fetchAll(\PDO::FETCH_COLUMN); } /** * {@inheritdoc} */ public function getAncestors($group_id) { - return $this->connection->query('SELECT start_vertex FROM {group_graph} WHERE end_vertex = :group_id', [ - ':group_id' => $group_id, - ])->fetchAll(\PDO::FETCH_COLUMN); + $query = $this->connection->select('group_graph', 'gg') + ->fields('gg', ['start_vertex']); + $query->condition('end_vertex', $group_id); + return $query->execute()->fetchAll(\PDO::FETCH_COLUMN); } /** @@ -307,10 +301,65 @@ EOT; * {@inheritdoc} */ public function isDescendant($a, $b) { - return $this->connection->query('SELECT COUNT(id) FROM {group_graph} WHERE start_vertex = :b AND end_vertex = :a', [ - ':a' => $a, - ':b' => $b, - ])->fetchField() > 0; + $query = $this->connection->select('group_graph', 'gg'); + $query->addExpression('COUNT(gg.id)'); + $query->condition('start_vertex', $b); + $query->condition('end_vertex', $a); + return $query->execute()->fetchField() > 0; + } + + /** + * {@inheritdoc} + */ + public function getPath($parent_group_id, $child_group_id) { + if (!$this->isAncestor($parent_group_id, $child_group_id)) { + return []; + } + + $visited = []; + $solutions = []; + + // Enqueue the origin vertex and mark as visited. + $queue = new \SplQueue(); + $queue->enqueue($child_group_id); + $visited[$child_group_id] = TRUE; + + // This is used to track the path back from each node. + $paths = []; + $paths[$child_group_id][] = $child_group_id; + + // While queue is not empty and destination not found. + while (!$queue->isEmpty() && $queue->bottom() != $parent_group_id) { + $child_id = $queue->dequeue(); + + // Get parents for child in queue. + $query = $this->connection->select('group_graph', 'gg') + ->fields('gg', ['start_vertex']); + $query->condition('end_vertex', $child_id); + $query->condition('hops', 0); + $parent_ids = $query->execute()->fetchAll(\PDO::FETCH_COLUMN); + + foreach ($parent_ids as $parent_id) { + if ((int) $parent_id === (int) $parent_group_id) { + // Add this path to the list of solutions. + $solution = $paths[$child_id]; + $solution[] = $parent_id; + $solutions[] = $solution; + } + else { + if (!isset($visited[$parent_id])) { + // If not yet visited, enqueue parent id and mark as visited. + $queue->enqueue($parent_id); + $visited[$parent_id] = TRUE; + // Add parent to current path. + $paths[$parent_id] = $paths[$child_id]; + $paths[$parent_id][] = $parent_id; + } + } + } + } + + return $solutions; } -} \ No newline at end of file +} diff --git a/modules/ggroup/src/GroupHierarchyManager.php b/modules/ggroup/src/GroupHierarchyManager.php index 3dee962..2697e8f 100644 --- a/modules/ggroup/src/GroupHierarchyManager.php +++ b/modules/ggroup/src/GroupHierarchyManager.php @@ -3,10 +3,13 @@ namespace Drupal\ggroup; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\ggroup\Graph\GroupGraphStorageInterface; -use Drupal\ggroup\GroupHierarchyManagerInterface; +use Drupal\group\Entity\GroupContentType; use Drupal\group\Entity\GroupInterface; use Drupal\group\Entity\GroupContentInterface; +use Drupal\group\GroupMembershipCollection; +use Drupal\group\GroupMembership; /** * Manages the relationship between groups (as subgroups). @@ -28,6 +31,25 @@ class GroupHierarchyManager implements GroupHierarchyManagerInterface { protected $entityTypeManager; /** + * Static cache for plugin config of all installed subgroup content types. + * + * A nested array with all configuration values for each subgroup content + * type. The array is keyed by subgroup content type ID. + * + * @var array[] + */ + protected $subgroupContentTypeConfig = []; + + /** + * Static cache of a user's inherited group role IDs. + * + * This nested array is keyed by user ID and group ID. + * + * @var \Drupal\group\Entity\GroupRoleInterface[][] + */ + protected $inheritedUserGroupRoleIds = []; + + /** * Constructs a new GroupHierarchyManager. * * @param \Drupal\ggroup\Graph\GroupGraphStorageInterface $group_graph_storage @@ -43,6 +65,55 @@ class GroupHierarchyManager implements GroupHierarchyManagerInterface { /** * {@inheritdoc} */ + public function addSubgroup(GroupContentInterface $group_content) { + $plugin = $group_content->getContentPlugin(); + + if ($plugin->getEntityTypeId() !== 'group') { + throw new \InvalidArgumentException('Given group content entity does not represent a subgroup relationship.'); + } + + $parent_group = $group_content->getGroup(); + /** @var \Drupal\group\Entity\GroupInterface $child_group */ + $child_group = $group_content->getEntity(); + + if ($parent_group->id() === NULL) { + throw new \InvalidArgumentException('Parent group must be saved before it can be related to another group.'); + } + + if ($child_group->id() === NULL) { + throw new \InvalidArgumentException('Child group must be saved before it can be related to another group.'); + } + + $new_edge_id = $this->groupGraphStorage->addEdge($parent_group->id(), $child_group->id()); + + // @todo Invalidate some kind of cache? + } + + /** + * {@inheritdoc} + */ + public function removeSubgroup(GroupContentInterface $group_content) { + $plugin = $group_content->getContentPlugin(); + + if ($plugin->getEntityTypeId() !== 'group') { + throw new \InvalidArgumentException('Given group content entity does not represent a subgroup relationship.'); + } + + $parent_group = $group_content->getGroup(); + + $child_group_id = $group_content->get('entity_id')->getValue(); + + if (!empty($child_group_id)) { + $child_group_id = reset($child_group_id)['target_id']; + $this->groupGraphStorage->removeEdge($parent_group->id(), $child_group_id); + } + + // @todo Invalidate some kind of cache? + } + + /** + * {@inheritdoc} + */ public function groupHasSubgroup(GroupInterface $group, GroupInterface $subgroup) { return $this->groupGraphStorage->isDescendant($subgroup->id(), $group->id()); } @@ -80,50 +151,244 @@ class GroupHierarchyManager implements GroupHierarchyManagerInterface { /** * {@inheritdoc} */ - public function addSubgroup(GroupContentInterface $group_content) { - $plugin = $group_content->getContentPlugin(); + public function getInheritedGroupRoles(GroupInterface $group, AccountInterface $account) { + if (isset($inheritedUserGroupRoleIds[$account->id()][$group->id()])) { + return $inheritedUserGroupRoleIds[$account->id()][$group->id()]; + } - if ($plugin->getEntityTypeId() !== 'group') { - throw new \InvalidArgumentException('Given group content entity does not represent a subgroup relationship.'); + $role_ids = []; + + $memberships = $this->loadUserDirectMemberships($account); + foreach ($memberships as $membership) { + $account_group = $membership->getGroup(); + $account_group_role_ids = $this->getMembershipRoles($membership); + + // Return loaded roles if user is a direct member of the group. + if ($account_group->id() === $group->id()) { + $role_ids = array_merge($account_group_role_ids); + continue; + } + + $role_ids = array_merge($this->getInheritedSupergroupRoleIds($account_group, $group, $account_group_role_ids), $role_ids); + $role_ids = array_merge($this->getInheritedSubgroupRoleIds($account_group, $group, $account_group_role_ids), $role_ids); } - $parent_group = $group_content->getGroup(); - /** @var \Drupal\group\Entity\GroupInterface $child_group */ - $child_group = $group_content->getEntity(); + return $inheritedUserGroupRoleIds[$account->id()][$group->id()] = $this->entityTypeManager->getStorage('group_role')->loadMultiple($role_ids); + } - if ($parent_group->id() === NULL) { - throw new \InvalidArgumentException('Parent group must be saved before it can be related to another group.'); + /** + * Get the plugin config for all installed subgroup content types. + * + * @return array[] + * A nested array with all configuration values for each subgroup content + * type. The array is keyed by subgroup content type ID. + */ + protected function getSubGroupContentTypeConfig() { + if (!$this->subgroupContentTypeConfig) { + foreach ($this->entityTypeManager->getStorage('group_type')->loadMultiple() as $group_type) { + $plugin_id = 'subgroup:' . $group_type->id(); + $subgroup_content_types = GroupContentType::loadByContentPluginId($plugin_id); + foreach ($subgroup_content_types as $subgroup_content_type) { + /** @var \Drupal\group\Entity\GroupContentTypeInterface $subgroup_content_type */ + $this->subgroupContentTypeConfig[$subgroup_content_type->id()] = $subgroup_content_type->getContentPlugin()->getConfiguration(); + } + } } + return $this->subgroupContentTypeConfig; + } - if ($child_group->id() === NULL) { - throw new \InvalidArgumentException('Child group must be saved before it can be related to another group.'); + /** + * Loads all groups where a user is a direct member. + * + * @param \Drupal\Core\Session\AccountInterface $account + * The user to load the membership for. + * + * @return \Drupal\group\GroupMembership[] + * An array containing all direct memberships. + * + * @todo: We can't user the membershiploader since we only want "real" + * memberships. The list in the membershiploader is being altered. We + * might need to add a way to get an unaltered membership list from the + * membershiploader. + * + * @see \Drupal\group\GroupMembershipLoader::loadByUser() + */ + protected function loadUserDirectMemberships(AccountInterface $account) { + // Load all group content types for the membership content enabler plugin. + $group_content_types = $this->entityTypeManager + ->getStorage('group_content_type') + ->loadByProperties(['content_plugin' => 'group_membership']); + + // If none were found, there can be no memberships either. + if (empty($group_content_types)) { + return []; } - $new_edge_id = $this->groupGraphStorage->addEdge($parent_group->id(), $child_group->id()); + // Try to load all possible membership group content for the user. + $group_content_type_ids = []; + foreach ($group_content_types as $group_content_type) { + $group_content_type_ids[] = $group_content_type->id(); + } - // @todo Invalidate some kind of cache? + $properties = ['type' => $group_content_type_ids, 'entity_id' => $account->id()]; + + /** @var \Drupal\group\GroupMembership[] $group_contents */ + $group_contents = $this->entityTypeManager->getStorage('group_content') + ->loadByProperties($properties); + $memberships = new GroupMembershipCollection(); + $memberships->addGroupContents($group_contents); + + return $memberships->allValues(); } /** - * {@inheritdoc} + * Get the role IDs for a group membership. + * + * @param \Drupal\group\GroupMembership $membership + * The user to load the membership for. + * + * @return string[] + * An array of role IDs. */ - public function removeSubgroup(GroupContentInterface $group_content) { - $plugin = $group_content->getContentPlugin(); + protected function getMembershipRoles(GroupMembership $membership) { + $ids = []; + foreach ($membership->getGroupContent()->group_roles as $group_role_ref) { + $ids[] = $group_role_ref->target_id; + } - if ($plugin->getEntityTypeId() !== 'group') { - throw new \InvalidArgumentException('Given group content entity does not represent a subgroup relationship.'); + // Add the implied group role IDs. + $ids[] = $membership->getGroup()->getGroupType()->getMemberRoleId(); + + return $ids; + } + + /** + * Get all inherited group roles for a group and a user account. + * + * We map the roles down for each relation in the full path between the + * original group and the supergroup. The result contains a list of all roles + * we have inherited from 1 or more supergroups. + * + * @param \Drupal\group\Entity\GroupInterface $supergroup + * The group for which inherited roles will be loaded. + * @param \Drupal\group\Entity\GroupInterface $group + * An account to map only the roles for a specific user. + * @param string[] $group_roles_to_map + * An array of group roles we want to down from the supergroup. + * + * @return string[] + * An array of group roles IDs for the group. + */ + protected function getInheritedSupergroupRoleIds(GroupInterface $supergroup, GroupInterface $group, array $group_roles_to_map) { + $subgroup_content_type_configurations = $this->getSubGroupContentTypeConfig(); + + $role_ids = []; + + $paths = $this->groupGraphStorage->getPath($supergroup->id(), $group->id()); + foreach ($paths as $path) { + $inherited_role_ids = $group_roles_to_map; + // Reverse the path since the list we want to start from the + // supergroups. + $reversed_path = array_reverse($path); + foreach ($reversed_path as $key => $supergroup_id) { + // We reached the end of the path, return mapped role IDs for group. + if ($supergroup_id === $group->id()) { + $role_ids = array_merge($role_ids, $inherited_role_ids); + break; + } + + // Get the subgroup ID from the next element. + $subgroup_id = isset($reversed_path[$key + 1]) ? $reversed_path[$key + 1] : NULL; + + // Get relation type between the supergroup and subgroup. + $group_content = $this->entityTypeManager->getStorage('group_content') + ->loadByProperties([ + 'type' => array_keys($subgroup_content_type_configurations), + 'gid' => $supergroup_id, + 'entity_id' => $subgroup_id, + ]); + $group_content = array_shift($group_content); + + // Get mapped roles for relation type. Filter array to remove + // unmapped roles. + $mapped_parent_roles = array_filter($subgroup_content_type_configurations[$group_content->bundle()]['parent_role_mapping']); + + // Check if we have a role to inherit and save a list of inherited + // subgroup roles for the next iteration. + $subgroup_role_ids = []; + foreach ($inherited_role_ids as $group_role_id) { + if (isset($mapped_parent_roles[$group_role_id])) { + $subgroup_role_ids[] = $mapped_parent_roles[$group_role_id]; + } + } + $inherited_role_ids = $subgroup_role_ids; + } } - $parent_group = $group_content->getGroup(); + return $role_ids; + } - $child_group_id = $group_content->get('entity_id')->getValue(); + /** + * Get all inherited group roles for a group and a user account. + * + * We map the roles up for each relation in the full path between the + * original group and the subgroup. The result contains a list of all roles + * we have inherited from 1 or more subgroups. + * + * @param \Drupal\group\Entity\GroupInterface $subgroup + * The group for which inherited roles will be loaded. + * @param \Drupal\group\Entity\GroupInterface $group + * An account to map only the roles for a specific user. + * @param string[] $group_roles_to_map + * An array of group roles we want to up from the subgroup. + * + * @return string[] + * An array of group roles IDs for the group. + */ + protected function getInheritedSubgroupRoleIds(GroupInterface $subgroup, GroupInterface $group, array $group_roles_to_map) { + $subgroup_content_type_configurations = $this->getSubGroupContentTypeConfig(); - if (!empty($child_group_id)) { - $child_group_id = reset($child_group_id)['target_id']; - $this->groupGraphStorage->removeEdge($parent_group->id(), $child_group_id); + $role_ids = []; + + $paths = $this->groupGraphStorage->getPath($group->id(), $subgroup->id()); + foreach ($paths as $path) { + $inherited_role_ids = $group_roles_to_map; + foreach ($path as $key => $subgroup_id) { + // We reached the end of the path, store mapped role IDs. + if ($subgroup_id === $group->id()) { + $role_ids = array_merge($role_ids, $inherited_role_ids); + break; + } + + // Get the subgroup ID from the next element. + $supergroup_id = isset($path[$key + 1]) ? $path[$key + 1] : NULL; + + // Get relation type between the supergroup and subgroup. + $group_content = $this->entityTypeManager->getStorage('group_content') + ->loadByProperties([ + 'type' => array_keys($subgroup_content_type_configurations), + 'gid' => $supergroup_id, + 'entity_id' => $subgroup_id, + ]); + $group_content = array_shift($group_content); + + // Get mapped roles for relation type. Filter array to remove + // unmapped roles. + $mapped_child_roles = array_filter($subgroup_content_type_configurations[$group_content->bundle()]['child_role_mapping']); + + // Check if we have a role to inherit and save a list of inherited + // supergroup roles for the next iteration. + $supergroup_role_ids = []; + foreach ($inherited_role_ids as $group_role_id) { + if (isset($mapped_child_roles[$group_role_id])) { + $supergroup_role_ids[] = $mapped_child_roles[$group_role_id]; + } + } + $inherited_role_ids = $supergroup_role_ids; + } } - // @todo Invalidate some kind of cache? + return $role_ids; } } diff --git a/modules/ggroup/src/GroupHierarchyManagerInterface.php b/modules/ggroup/src/GroupHierarchyManagerInterface.php index fc0b5dd..047c58b 100644 --- a/modules/ggroup/src/GroupHierarchyManagerInterface.php +++ b/modules/ggroup/src/GroupHierarchyManagerInterface.php @@ -2,6 +2,7 @@ namespace Drupal\ggroup; +use Drupal\Core\Session\AccountInterface; use Drupal\group\Entity\GroupInterface; use Drupal\group\Entity\GroupContentInterface; @@ -13,7 +14,7 @@ interface GroupHierarchyManagerInterface { /** * Relates one group to another as a subgroup. * - * @param GroupContentInterface $group_content + * @param \Drupal\group\Entity\GroupContentInterface $group_content * The group content representing the subgroup relationship. */ public function addSubgroup(GroupContentInterface $group_content); @@ -21,20 +22,20 @@ interface GroupHierarchyManagerInterface { /** * Removes the relationship of a subgroup. * - * @param GroupContentInterface $group_content + * @param \Drupal\group\Entity\GroupContentInterface $group_content * The group content representing the subgroup relationship. */ public function removeSubgroup(GroupContentInterface $group_content); /** - * Checks if a given group has another group as a subgroup anywhere in its - * descendent subgroups. + * Checks if a group has a subgroup anywhere in its descendents. * - * @param GroupInterface $group + * @param \Drupal\group\Entity\GroupInterface $group * The parent group whose subgroups will be checked. - * @param GroupInterface $subgroup + * @param \Drupal\group\Entity\GroupInterface $subgroup * The subgroup that will be searched for within the parent group's * subgroups. + * * @return bool * TRUE if the given group has the given subgroup, or FALSE if not. */ @@ -43,9 +44,10 @@ interface GroupHierarchyManagerInterface { /** * Loads the subgroups of a given group. * - * @param GroupInterface $group + * @param \Drupal\group\Entity\GroupInterface $group * The group for which subgroups will be loaded. - * @return GroupInterface[] + * + * @return \Drupal\group\Entity\GroupInterface[] * An array of subgroups for the given group. */ public function getGroupSubgroups(GroupInterface $group); @@ -53,8 +55,9 @@ interface GroupHierarchyManagerInterface { /** * Gets the IDs of the subgroups of a given group. * - * @param GroupInterface $group + * @param \Drupal\group\Entity\GroupInterface $group * The group for which subgroups will be loaded. + * * @return int[] * An array of subgroup IDs for the given group. */ @@ -63,9 +66,10 @@ interface GroupHierarchyManagerInterface { /** * Loads the supergroups of a given group. * - * @param GroupInterface $group + * @param \Drupal\group\Entity\GroupInterface $group * The group for which supergroups will be loaded. - * @return GroupInterface[] + * + * @return \Drupal\group\Entity\GroupInterface[] * An array of supergroups for the given group. */ public function getGroupSupergroups(GroupInterface $group); @@ -73,11 +77,32 @@ interface GroupHierarchyManagerInterface { /** * Gets the IDs of the supergroups of a given group. * - * @param GroupInterface $group + * @param \Drupal\group\Entity\GroupInterface $group * The group for which supergroups will be loaded. + * * @return int[] * An array of supergroup IDs for the given group. */ public function getGroupSupergroupIds(GroupInterface $group); + /** + * Get all (inherited) group roles for a group and a user account. + * + * Check if the account is a direct member of any subgroups/supergroups of + * the group. For each subgroup/supergroup, we check which roles we are + * allowed to map. We map the roles up/down for each relation in the full + * path between the original group and the subgroup/supergroup. The result + * contains a list of all roles we have inherited from 1 or more subgroups or + * supergroups. + * + * @param \Drupal\group\Entity\GroupInterface $group + * The group for which inherited roles will be loaded. + * @param \Drupal\Core\Session\AccountInterface $account + * An account to map only the roles for a specific user. + * + * @return \Drupal\group\Entity\GroupRoleInterface[] + * An array of group roles for the given group. + */ + public function getInheritedGroupRoles(GroupInterface $group, AccountInterface $account); + } diff --git a/modules/ggroup/src/GroupMembershipSubscriber.php b/modules/ggroup/src/GroupMembershipSubscriber.php index ec101ad..0924ee5 100644 --- a/modules/ggroup/src/GroupMembershipSubscriber.php +++ b/modules/ggroup/src/GroupMembershipSubscriber.php @@ -3,7 +3,6 @@ namespace Drupal\ggroup; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\ggroup\GroupHierarchyManagerInterface; use Drupal\group\GroupMembershipCollection; use Drupal\group\GroupMembershipLoaderEvents; use Drupal\group\GroupMembershipLoaderByGroupEvent; @@ -13,8 +12,7 @@ use Drupal\group\Entity\GroupContent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; /** - * Reacts to group membership loader events to add indirect memberships to - * groups based on group hierarchy. + * Reacts to membership loader events to add indirect memberships to groups. */ class GroupMembershipSubscriber implements EventSubscriberInterface { @@ -40,7 +38,7 @@ class GroupMembershipSubscriber implements EventSubscriberInterface { * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. */ - public function __construct(GroupHierarchyManager $group_hierarchy_manager, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(GroupHierarchyManagerInterface $group_hierarchy_manager, EntityTypeManagerInterface $entity_type_manager) { $this->groupHierarchyManager = $group_hierarchy_manager; $this->entityTypeManager = $entity_type_manager; } @@ -58,21 +56,26 @@ class GroupMembershipSubscriber implements EventSubscriberInterface { } /** - * Adds additional memberships to the membership collection based on group - * hierarchy. Finds all memberships in the event group's subgroups and adds + * Adds additional memberships to the membership collection. + * + * Finds all memberships in the event group's subgroups and adds * corresponding indirect memberships (as unsaved group content entities) to * the membership collection for the group. * - * @param GroupMembershipLoaderByGroupEvent $event + * @param \Drupal\group\GroupMembershipLoaderByGroupEvent $event * The event data, including the group and the group's direct memberships. */ public function onAlterMembershipsByGroup(GroupMembershipLoaderByGroupEvent $event) { - $memberships = $event->getMemberships(); $group = $event->getGroup(); - $plugin = $group->getGroupType()->getContentPlugin('group_membership'); - $subgroup_ids = $this->groupHierarchyManager->getGroupSubgroupIds($group); + $memberships = $event->getMemberships(); + $filter_roles = $event->getRoles(); - if (empty($subgroup_ids)) { + $inherited_group_ids = []; + $inherited_group_ids = array_merge($inherited_group_ids, $this->groupHierarchyManager->getGroupSupergroupIds($group)); + $inherited_group_ids = array_merge($inherited_group_ids, $this->groupHierarchyManager->getGroupSubgroupIds($group)); + $inherited_group_ids = array_unique($inherited_group_ids); + + if (empty($inherited_group_ids)) { return; } @@ -83,106 +86,69 @@ class GroupMembershipSubscriber implements EventSubscriberInterface { // If none were found, there can be no memberships either. if (empty($group_content_types)) { - return []; - } - - // Try to load all possible membership group content. - $group_content_type_ids = []; - foreach ($group_content_types as $group_content_type) { - $group_content_type_ids[] = $group_content_type->id(); + return; } - $properties = ['type' => $group_content_type_ids, 'gid' => $subgroup_ids]; - - /** @var \Drupal\group\Entity\GroupContentInterface[] $group_contents */ + // Try to load all possible membership group content for inherited + // subgroups/supergroups. + $properties = ['type' => array_keys($group_content_types), 'gid' => $inherited_group_ids]; $group_contents = $this->entityTypeManager ->getStorage('group_content') ->loadByProperties($properties); - $existing_membership_users = []; - + // Get a list of users already a member of the group. + $existing_members = []; foreach ($memberships as $membership) { - $existing_membership_users[] = $membership->getUser()->id(); + $existing_members[] = $membership->getUser()->id(); } + /** @var \Drupal\group\Entity\GroupContentInterface[] $group_contents */ foreach ($group_contents as $group_content) { - $user_id = $group_content->getEntity()->id(); + /** @var \Drupal\Core\Session\AccountInterface $inherited_user */ + $inherited_user = $group_content->getEntity(); // Don't want to create another indirect group membership instance if the // user is already a member. - // @todo Fold indirect membership roles into the existing membership - // objects once roles are being mapped properly. - if (in_array($user_id, $existing_membership_users)) { + if (in_array($inherited_user->id(), $existing_members)) { continue; } - $existing_membership_users[] = $user_id; - - $properties = [ - 'type' => $plugin->getContentTypeConfigId(), - 'gid' => $group->id(), - 'entity_id' => $user_id, - ]; - - $membership = GroupContent::create($properties); - $memberships->addGroupContent($membership); + $existing_members[] = $inherited_user->id(); + + // Check if the user has any roles to inherit on the group. When no + // role filter is applied, return all inherited roles. + if ($roles = $this->groupHierarchyManager->getInheritedGroupRoles($group, $inherited_user)) { + $filtered_roles = ($filter_roles) ? array_intersect(array_keys($roles), $filter_roles) : $roles; + if ($filtered_roles) { + $properties = [ + 'type' => $group->getGroupType()->getContentPlugin('group_membership')->getContentTypeConfigId(), + 'gid' => $group->id(), + 'entity_id' => $inherited_user->id(), + 'group_roles' => $filtered_roles, + ]; + /** @var \Drupal\group\Entity\GroupContentInterface $membership */ + $membership = GroupContent::create($properties); + $memberships->addGroupContent($membership); + } + } } } /** - * Adds additional memberships to the membership collection based on group - * hierarchy. Uses all memberships of the event user to find supergroups of + * Adds additional memberships to the membership collection. + * + * Uses all memberships of the event user to find subgroups/supergroups of * those membership groups and then adds corresponding indirect memberships * (as unsaved group content entities) to the membership collection for the * user. * - * @param GroupMembershipLoaderByGroupEvent $event + * @param \Drupal\group\GroupMembershipLoaderByUserEvent $event * The event data, including the user and the user's direct memberships. */ public function onAlterMembershipsByUser(GroupMembershipLoaderByUserEvent $event) { $user = $event->getUser(); $memberships = $event->getMemberships(); - $supergroup_ids = []; - $user_groups_ids = []; - foreach ($memberships as $membership) { - $group = $membership->getGroup(); - $user_groups_ids[] = $group->id(); - $supergroup_ids = array_merge($supergroup_ids, $this->groupHierarchyManager->getGroupSupergroupIds($group)); - } - $supergroup_ids = array_unique($supergroup_ids); - $supergroup_ids = array_diff($supergroup_ids, $user_groups_ids); - if (empty($supergroup_ids)) { - return; - } - - $supergroups = $this->entityTypeManager - ->getStorage('group') - ->loadMultiple($supergroup_ids); - - foreach ($supergroups as $supergroup_id => $supergroup) { - $properties = [ - 'type' => $supergroup->getGroupType()->getContentPlugin('group_membership')->getContentTypeConfigId(), - 'gid' => $supergroup_id, - 'entity_id' => $user->id(), - ]; - $membership = GroupContent::create($properties); - $memberships->addGroupContent($membership); - } - } - - /** - * Adds additional memberships to the membership collection based on group - * hierarchy. Uses the event user and event group load direct memberships for - * the user then looks for the given group among the supergroups of those user - * memberships. If the group is found, an indirect membership (as an unsaved - * group content entity) is added to the membership collection for the user. - * - * @param GroupMembershipLoaderByUserGroupEvent $event - * The event data, including user, group, and direct membership. - */ - public function onAlterMembershipsByUserAndGroup(GroupMembershipLoaderByUserGroupEvent $event) { - // @todo Most of this method is code copied from - // GroupMembershipLoader::loadByUser() and should be refactored. + $filter_roles = $event->getRoles(); // Load all group content types for the membership content enabler plugin. $group_content_types = $this->entityTypeManager @@ -191,51 +157,84 @@ class GroupMembershipSubscriber implements EventSubscriberInterface { // If none were found, there can be no memberships either. if (empty($group_content_types)) { - return []; - } - - // Try to load all possible membership group content for the user. - $group_content_type_ids = []; - foreach ($group_content_types as $group_content_type) { - $group_content_type_ids[] = $group_content_type->id(); + return; } - $properties = [ - 'type' => $group_content_type_ids, - 'entity_id' => $event->getUser()->id() - ]; - + $properties = ['type' => array_keys($group_content_types), 'entity_id' => $user->id()]; /** @var \Drupal\group\Entity\GroupContentInterface[] $group_contents */ - $group_contents = $this->entityTypeManager - ->getStorage('group_content') + $group_contents = $this->entityTypeManager->getStorage('group_content') ->loadByProperties($properties); + $original_memberships = new GroupMembershipCollection(); + $original_memberships->addGroupContents($group_contents); - $supergroup_ids = []; - $supergroup_id = $event->getGroup()->id(); - - foreach ($group_contents as $group_content) { - $group = $group_content->getGroup(); - $supergroup_ids = array_merge($supergroup_ids, $this->groupHierarchyManager->getGroupSupergroupIds($group)); + $inherited_group_ids = []; + foreach ($original_memberships->allValues() as $membership) { + /** @var \Drupal\group\GroupMembership $membership */ + $group = $membership->getGroup(); + $inherited_group_ids[] = $group->id(); + $inherited_group_ids = array_merge($inherited_group_ids, $this->groupHierarchyManager->getGroupSupergroupIds($group)); + $inherited_group_ids = array_merge($inherited_group_ids, $this->groupHierarchyManager->getGroupSubgroupIds($group)); } + $inherited_group_ids = array_unique($inherited_group_ids); - $supergroup_ids = array_unique($supergroup_ids); - - if (!in_array($supergroup_id, $supergroup_ids)) { + if (empty($inherited_group_ids)) { return; } - $supergroup = $this->entityTypeManager + $inherited_groups = $this->entityTypeManager ->getStorage('group') - ->load($supergroup_id); + ->loadMultiple($inherited_group_ids); + + foreach ($inherited_groups as $inherited_group_id => $inherited_group) { + /** @var \Drupal\group\Entity\GroupInterface $inherited_group */ + // Check if the user has any roles to inherit on the group. When no + // role filter is applied, return all inherited roles. + if ($roles = $this->groupHierarchyManager->getInheritedGroupRoles($inherited_group, $user)) { + $filtered_roles = ($filter_roles) ? array_intersect(array_keys($roles), $filter_roles) : $roles; + if ($filtered_roles) { + $properties = [ + 'type' => $inherited_group->getGroupType()->getContentPlugin('group_membership')->getContentTypeConfigId(), + 'gid' => $inherited_group_id, + 'entity_id' => $user->id(), + 'group_roles' => $filtered_roles, + ]; + /** @var \Drupal\group\Entity\GroupContentInterface $membership */ + $membership = GroupContent::create($properties); + $memberships->addGroupContent($membership); + } + } + } + } - $properties = [ - 'type' => $supergroup->getGroupType()->getContentPlugin('group_membership')->getContentTypeConfigId(), - 'gid' => $supergroup_id, - 'entity_id' => $event->getUser()->id(), - ]; + /** + * Adds additional memberships to the membership collection. + * + * Uses the event user and event group load direct memberships for the user + * then looks for the given group among the supergroups of those user + * memberships. If the group is found, an indirect membership (as an unsaved + * group content entity) is added to the membership collection for the user. + * + * @param \Drupal\group\GroupMembershipLoaderByUserGroupEvent $event + * The event data, including user, group, and direct membership. + */ + public function onAlterMembershipsByUserAndGroup(GroupMembershipLoaderByUserGroupEvent $event) { + $group = $event->getGroup(); + $user = $event->getUser(); + $memberships = $event->getMemberships(); - $membership = GroupContent::create($properties); - $event->getMemberships()->addGroupContent($membership); + // Check if the user inherited any roles for this group. + /** @var \Drupal\group\Entity\GroupInterface $inherited_group */ + if ($roles = $this->groupHierarchyManager->getInheritedGroupRoles($group, $user)) { + $properties = [ + 'type' => $group->getGroupType()->getContentPlugin('group_membership')->getContentTypeConfigId(), + 'gid' => $group, + 'entity_id' => $user->id(), + 'group_roles' => array_keys($roles), + ]; + /** @var \Drupal\group\Entity\GroupContentInterface $membership */ + $membership = GroupContent::create($properties); + $memberships->addGroupContent($membership); + } } } diff --git a/modules/ggroup/src/Plugin/GroupContentEnabler/Subgroup.php b/modules/ggroup/src/Plugin/GroupContentEnabler/Subgroup.php index d42b34c..fb731d7 100644 --- a/modules/ggroup/src/Plugin/GroupContentEnabler/Subgroup.php +++ b/modules/ggroup/src/Plugin/GroupContentEnabler/Subgroup.php @@ -121,6 +121,50 @@ class Subgroup extends GroupContentEnablerBase { $form['entity_cardinality']['#disabled'] = TRUE; $form['entity_cardinality']['#description'] .= '
' . $info . ''; + // We create form field to map parent roles to child roles, and map child + // roles to parent roles. This allow for permissions/membership to + // propogate up/down. + $parent_roles = $this->getGroupType()->getRoles(); + $parent_options = []; + foreach ($parent_roles as $role_id => $role) { + $parent_options[$role_id] = $role->label(); + } + + $child_roles = $this->getSubgroupType()->getRoles(); + $child_options = []; + foreach ($child_roles as $role_id => $role) { + $child_options[$role_id] = $role->label(); + } + + $form['parent_role_mapping'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Map group roles to subgroup roles to allow group membership and permissions to be inherited by the subgroup.'), + '#tree' => TRUE, + ]; + foreach ($parent_options as $roleid => $rolename) { + $form['parent_role_mapping'][$roleid] = [ + '#type' => 'select', + '#title' => $rolename, + '#options' => $child_options, + "#empty_option" => $this->t('- None -'), + '#default_value' => $this->configuration['parent_role_mapping'][$roleid], + ]; + } + $form['child_role_mapping'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Map subgroup roles to group roles to allow subgroup membership and permissions to be propogated to the group.'), + '#tree' => TRUE, + ]; + foreach ($child_options as $roleid => $rolename) { + $form['child_role_mapping'][$roleid] = [ + '#type' => 'select', + '#title' => $rolename, + '#options' => $parent_options, + "#empty_option" => $this->t('- None -'), + '#default_value' => $this->configuration['child_role_mapping'][$roleid], + ]; + } + return $form; } diff --git a/src/Entity/Group.php b/src/Entity/Group.php index b00e756..0808b76 100644 --- a/src/Entity/Group.php +++ b/src/Entity/Group.php @@ -238,18 +238,6 @@ class Group extends ContentEntityBase implements GroupInterface { } } - // Check permission for supergroups if subgroup module is enabled. - $moduleHandler = \Drupal::service('module_handler'); - if ($moduleHandler->moduleExists('ggroup')){ - /** @var \Drupal\ggroup\GroupHierarchyManager $group_hierarchy_manager */ - $supergroups = \Drupal::service('ggroup.group_hierarchy_manager')->getGroupSupergroups($this); - foreach ($supergroups as $group) { - if ($group->hasPermission($permission, $account)) { - return TRUE; - } - } - } - // If no role had the requested permission, we deny access. return FALSE; } diff --git a/src/GroupMembershipLoader.php b/src/GroupMembershipLoader.php index a36e2fa..d317e44 100644 --- a/src/GroupMembershipLoader.php +++ b/src/GroupMembershipLoader.php @@ -5,7 +5,6 @@ namespace Drupal\group; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\group\Entity\GroupInterface; -use Drupal\group\GroupMembershipLoaderEvents; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** @@ -51,7 +50,7 @@ class GroupMembershipLoader implements GroupMembershipLoaderInterface { * The entity type manager. * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. - * @param \Symfony\Component\EventDispatcher\EventDispatchInterface $event_dispatcher + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher. */ public function __construct(EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user, EventDispatcherInterface $event_dispatcher) { @@ -69,7 +68,6 @@ class GroupMembershipLoader implements GroupMembershipLoaderInterface { return $this->entityTypeManager->getStorage('group_content'); } - /** * {@inheritdoc} */ diff --git a/src/GroupMembershipLoaderByGroupEvent.php b/src/GroupMembershipLoaderByGroupEvent.php index 1aead24..59650f4 100644 --- a/src/GroupMembershipLoaderByGroupEvent.php +++ b/src/GroupMembershipLoaderByGroupEvent.php @@ -3,11 +3,11 @@ namespace Drupal\group; use Drupal\group\Entity\GroupInterface; -use Drupal\group\GroupMembershipCollection; use Symfony\Component\EventDispatcher\Event; /** - * An event dispatched when a group's user membership is being loaded. + * An event dispatched when a group's memberships are being loaded. + * * Subscribers may affect the group membership by modifying the * GroupMembershipCollection returned by getMemberships(). * @@ -82,4 +82,4 @@ class GroupMembershipLoaderByGroupEvent extends Event { return $this->roles; } -} \ No newline at end of file +} diff --git a/src/GroupMembershipLoaderByUserEvent.php b/src/GroupMembershipLoaderByUserEvent.php index 228de1c..afb3fb1 100644 --- a/src/GroupMembershipLoaderByUserEvent.php +++ b/src/GroupMembershipLoaderByUserEvent.php @@ -3,11 +3,11 @@ namespace Drupal\group; use Drupal\Core\Session\AccountInterface; -use Drupal\group\GroupMembershipCollection; use Symfony\Component\EventDispatcher\Event; /** - * An event dispatched when a user's group membership is being loaded. + * An event dispatched when a user's memberships are being loaded. + * * Subscribers may affect the group membership by modifying the * GroupMembershipCollection returned by getMemberships(). * @@ -65,7 +65,7 @@ class GroupMembershipLoaderByUserEvent extends Event { /** * Get the memberships for the user to which the event corresponds. * - * @return \Drupal\group\MembreshipCollection + * @return \Drupal\group\GroupMembershipCollection * The collection of memberships for the user. */ public function getMemberships() { @@ -82,4 +82,4 @@ class GroupMembershipLoaderByUserEvent extends Event { return $this->roles; } -} \ No newline at end of file +} diff --git a/src/GroupMembershipLoaderByUserGroupEvent.php b/src/GroupMembershipLoaderByUserGroupEvent.php index 88b099f..07e0d5d 100644 --- a/src/GroupMembershipLoaderByUserGroupEvent.php +++ b/src/GroupMembershipLoaderByUserGroupEvent.php @@ -4,12 +4,12 @@ namespace Drupal\group; use Drupal\Core\Session\AccountInterface; use Drupal\group\Entity\GroupInterface; -use Drupal\group\GroupMembershipCollection; use Symfony\Component\EventDispatcher\Event; /** - * An event dispatched when a user's group membership is being loaded for a - * specific group. Subscribers may affect the group membership by modifying the + * An event dispatched when a user's membership is being loaded for a group. + * + * Subscribers may affect the group membership by modifying the * GroupMembershipCollection returned by getMemberships(). * * @see \Drupal\group\GroupMembershipLoader::load() @@ -83,4 +83,4 @@ class GroupMembershipLoaderByUserGroupEvent extends Event { return $this->group; } -} \ No newline at end of file +} diff --git a/src/GroupMembershipLoaderEvents.php b/src/GroupMembershipLoaderEvents.php index 0ce0838..b517f5e 100644 --- a/src/GroupMembershipLoaderEvents.php +++ b/src/GroupMembershipLoaderEvents.php @@ -8,21 +8,18 @@ namespace Drupal\group; final class GroupMembershipLoaderEvents { /** - * This event gives modules the opportunity to alter group memberships that - * were loaded by group. + * Add event to alter group memberships loaded by group. */ const ALTER_BY_GROUP = 'group.membership_loader.alter_by_group'; /** - * This event gives modules the opportunity to alter group memberships that - * were loaded by user. + * Add event to alter group memberships loaded by user. */ const ALTER_BY_USER = 'group.membership_loader.alter_by_user'; /** - * This event gives modules the opportunity to alter group memberships that - * were loaded by the combination of group and user. + * Add event to alter group memberships loaded by group/user. */ const ALTER_BY_USER_GROUP = 'group.membership_loader.alter_by_user_group'; -} \ No newline at end of file +}