diff --git a/og_access/og_access.info.yml b/og_access/og_access.info.yml new file mode 100644 index 000000000..45cb18ce1 --- /dev/null +++ b/og_access/og_access.info.yml @@ -0,0 +1,9 @@ +name: Organic Groups access control +description: "Enable access control for private and public groups and group content." +package: Organic Groups + +core_version_requirement: ^8 || ^9 +type: module + +dependencies: + - og:og diff --git a/og_access/og_access.install b/og_access/og_access.install new file mode 100644 index 000000000..c39a15be7 --- /dev/null +++ b/og_access/og_access.install @@ -0,0 +1,16 @@ +getUserGroups($account->id())) { + foreach ($groups as $group_type => $entity_groups) { + /** @var \Drupal\core\Entity\EntityInterface $group */ + foreach ($entity_groups as $group) { + $realm = OgAccess::OG_ACCESS_REALM . ':' . $group_type; + $grants[$realm][] = $group->id(); + } + } + } + + return !empty($grants) ? $grants : []; +} + +/** + * Implements hook_node_access_records(). + */ +function og_access_node_access_records(NodeInterface $node) { + if (!$node->isPublished()) { + // Node is unpublished, so we don't allow any group member to see it. + return []; + } + + // The group IDs, that in case access is granted, will be recorded. + $gids = []; + + if (Og::isGroup('node', $node->getType()) && + $node->hasField(OgAccess::OG_ACCESS_FIELD) && + !empty($node->{OgAccess::OG_ACCESS_FIELD}) && $node->{OgAccess::OG_ACCESS_FIELD}->value) { + // Private group. + $gids['node'][] = $node->id(); + } + + if ($node->hasField(OgAccess::OG_ACCESS_CONTENT_FIELD) && + !empty($node->get(OgAccess::OG_ACCESS_CONTENT_FIELD))) { + $content_access = $node->get(OgAccess::OG_ACCESS_CONTENT_FIELD)->value; + } + else { + $content_access = OgAccess::OG_ACCESS_PUBLIC; + } + + switch ($content_access) { + case OgAccess::OG_ACCESS_PUBLIC: + // Skip non-group content nodes. + if (!Og::isGroupContent('node', $node->getType())) { + break; + } + + $has_private = FALSE; + /** @var \Drupal\og\OgGroupAudienceHelper $audience_helper */ + $audience_helper = \Drupal::service('og.group_audience_helper'); + foreach ($audience_helper->getAllGroupAudienceFields('node', $node->getType()) as $field_name => $field) { + foreach ($node->get($field_name)->referencedEntities() as $group) { + $list_gids[$group->getEntityTypeId()][] = $group->id(); + + if ($has_private) { + // We already know we have a private group, so we can avoid + // re-checking it. + continue; + } + + if ($group->hasField(OgAccess::OG_ACCESS_FIELD) && !empty($group->get(OgAccess::OG_ACCESS_FIELD)) && + $group->get(OgAccess::OG_ACCESS_FIELD)->value) { + $has_private = TRUE; + } + } + } + if ($has_private) { + $gids = array_merge_recursive($gids, $list_gids); + } + break; + + case OgAccess::OG_ACCESS_PRIVATE: + $list_gids = []; + /** @var \Drupal\og\OgGroupAudienceHelper $audience_helper */ + $audience_helper = \Drupal::service('og.group_audience_helper'); + foreach ($audience_helper->getAllGroupAudienceFields('node', $node->getType()) as $field_name => $field) { + foreach ($node->get($field_name)->referencedEntities() as $group) { + $list_gids[$group->getEntityTypeId()][] = $group->id(); + } + } + + $gids = array_merge_recursive($gids, $list_gids); + break; + } + + foreach ($gids as $group_type => $values) { + foreach ($values as $gid) { + $grants[] = [ + 'realm' => OgAccess::OG_ACCESS_REALM . ':' . $group_type, + 'gid' => $gid, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + ]; + } + } + + return !empty($grants) ? $grants : []; +} + +/** + * Implements hook_form_alter(). + */ +function og_access_form_alter(array &$form, FormStateInterface $form_state, $form_id) { + if ($form_state->getFormObject() instanceof BundleEntityFormBase) { + (new OgAccessBundleFormAlter($form_state->getFormObject()->getEntity(), \Drupal::service('string_translation'))) + ->formAlter($form, $form_state); + } +} + +/** + * Implements hook_entity_insert(). + */ +function og_access_entity_insert(EntityInterface $entity) { + og_access_entity_type_save($entity); +} + +/** + * Implements hook_entity_update(). + */ +function og_access_entity_update(EntityInterface $entity) { + og_access_entity_type_save($entity); +} + +/** + * Adds/removes the group and group content access fields. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity object. + */ +function og_access_entity_type_save(EntityInterface $entity) { + if (!$entity instanceof ConfigEntityBundleBase || !isset($entity->og_is_group)) { + return; + } + + $bundle = $entity->id(); + $definition = \Drupal::entityTypeManager()->getDefinition($entity->getEntityTypeId()); + $entity_type_id = $definition->getBundleOf(); + + $enable_og_access = $entity->og_enable_access; + + // Add/remove on the group itself. + $is_group = Og::isGroup($entity_type_id, $bundle); + if ($entity->og_is_group || $is_group) { + $field = FieldConfig::loadByName($entity_type_id, $bundle, OgAccess::OG_ACCESS_FIELD); + if (!$field && $enable_og_access) { + Og::createField(OgAccess::OG_ACCESS_FIELD, $entity_type_id, $bundle); + } + elseif ($field) { + if (!$enable_og_access || $is_group && !$entity->og_is_group) { + $field->delete(); + } + } + } + + // Add remove the relevant field to the group content bundle. + $is_group_content = Og::isGroupContent($entity_type_id, $bundle); + if ($entity->og_group_content_bundle || $is_group_content) { + $field = FieldConfig::loadByName($entity_type_id, $bundle, OgAccess::OG_ACCESS_CONTENT_FIELD); + + if (!$field && $enable_og_access) { + Og::createField(OgAccess::OG_ACCESS_CONTENT_FIELD, $entity_type_id, $bundle); + } + elseif ($field) { + if (!$enable_og_access || $is_group_content && !$entity->og_group_content_bundle) { + $field->delete(); + } + } + } +} + +/** + * Implements hook_module_implements_alter(). + */ +function og_access_module_implements_alter(&$implementations, $hook) { + if ($hook === 'form_alter') { + // Move our form alter after the og_ui one. + // @todo Remove once og_ui and og are merged. + $group = $implementations['og_access']; + unset($implementations['og_access']); + $implementations['og_access'] = $group; + } +} diff --git a/og_access/src/OgAccess.php b/og_access/src/OgAccess.php new file mode 100644 index 000000000..548a86796 --- /dev/null +++ b/og_access/src/OgAccess.php @@ -0,0 +1,37 @@ +entity = $entity; + $this->stringTranslation = $string_translation; + } + + /** + * This is a helper for og_ui_form_alter(). + * + * @param array $form + * The form variable. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + */ + public function formAlter(array &$form, FormStateInterface $form_state) { + // Example: node. + $this->entityTypeId = $this->entity->getEntityType()->getBundleOf(); + + // Example: article. + $this->bundle = $this->entity->id(); + + $form['og']['og_enable_access'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Restrict access to group members'), + '#description' => $this->t('Enable OG access control. Provides a new field that determines the group/group content visibility. Public groups can have member-only content. Any public group content belonging to a private group will be restricted to the members of that group only.'), + '#default_value' => $this->bundle ? $this->hasAccessControl() : FALSE, + '#states' => [ + 'visible' => [ + [':input[name="og_is_group"]' => ['checked' => TRUE]], + [':input[name="og_group_content_bundle"]' => ['checked' => TRUE]], + ], + ], + ]; + } + + /** + * Checks whether the existing bundle has OG access control enabled. + * + * @return bool + * True if the group bundle has the OgAccess::OG_ACCESS_FIELD field -OR- + * if the group content bundle has the OG_CONTENT_ACCESS_FIELD field. + * False otherwise. + */ + protected function hasAccessControl() { + $field_definitions = \Drupal::service('entity_field.manager') + ->getFieldDefinitions($this->entityTypeId, $this->bundle); + + if (Og::isGroup($this->entityTypeId, $this->bundle)) { + return isset($field_definitions[OgAccess::OG_ACCESS_FIELD]); + } + + if (Og::isGroupContent($this->entityTypeId, $this->bundle)) { + return isset($field_definitions[OgAccess::OG_ACCESS_CONTENT_FIELD]); + } + + return FALSE; + } + +} diff --git a/og_access/src/Plugin/OgFields/OgAccessField.php b/og_access/src/Plugin/OgFields/OgAccessField.php new file mode 100644 index 000000000..d0ba276d5 --- /dev/null +++ b/og_access/src/Plugin/OgFields/OgAccessField.php @@ -0,0 +1,80 @@ + 1, + 'settings' => [ + 'allowed_values' => [ + 0 => $this->t('Public - accessible to all site users'), + 1 => $this->t('Private - accessible only to group members'), + ], + 'allowed_values_function' => '', + ], + 'type' => 'list_integer', + ]; + + return parent::getFieldStorageBaseDefinition($values); + } + + /** + * {@inheritdoc} + */ + public function getFieldBaseDefinition(array $values = []) { + $values += [ + 'default_value' => [0 => ['value' => 0]], + 'description' => $this->t('Determine access to the group.'), + 'display_label' => TRUE, + 'label' => $this->t('Group visibility'), + 'required' => TRUE, + ]; + + return parent::getFieldBaseDefinition($values); + } + + /** + * {@inheritdoc} + */ + public function getFormDisplayDefinition(array $values = []) { + $values += [ + 'type' => 'options_buttons', + 'settings' => [], + 'default_value' => 0, + ]; + + return $values; + } + + /** + * {@inheritdoc} + */ + public function getViewDisplayDefinition(array $values = []) { + $values += [ + 'type' => 'list_default', + 'label' => 'above', + ]; + + return $values; + } + +} diff --git a/og_access/src/Plugin/OgFields/OgContentAccessField.php b/og_access/src/Plugin/OgFields/OgContentAccessField.php new file mode 100644 index 000000000..dc40cda58 --- /dev/null +++ b/og_access/src/Plugin/OgFields/OgContentAccessField.php @@ -0,0 +1,80 @@ + 1, + 'settings' => [ + 'allowed_values' => [ + 0 => $this->t('Public - accessible to all site users'), + 1 => $this->t('Private - accessible only to group members'), + ], + 'allowed_values_function' => '', + ], + 'type' => 'list_integer', + ]; + + return parent::getFieldStorageBaseDefinition($values); + } + + /** + * {@inheritdoc} + */ + public function getFieldBaseDefinition(array $values = []) { + $values += [ + 'default_value' => [0 => ['value' => 0]], + 'description' => $this->t('Determine the group content visibility.'), + 'display_label' => TRUE, + 'label' => $this->t('Group content visibility'), + 'required' => TRUE, + ]; + + return parent::getFieldBaseDefinition($values); + } + + /** + * {@inheritdoc} + */ + public function getFormDisplayDefinition(array $values = []) { + $values += [ + 'type' => 'options_buttons', + 'settings' => [], + 'default_value' => 0, + ]; + + return $values; + } + + /** + * {@inheritdoc} + */ + public function getViewDisplayDefinition(array $values = []) { + $values += [ + 'type' => 'list_default', + 'label' => 'above', + ]; + + return $values; + } + +}