You are here

og.module in Organic groups 8

File

og.module
View source
<?php

/**
 * @file
 * Enable users to create and manage groups with roles and permissions.
 */
declare (strict_types=1);
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\og\Entity\OgRole;
use Drupal\og\Og;
use Drupal\og\OgGroupAudienceHelperInterface;
use Drupal\og\OgMembershipInterface;
use Drupal\og\OgRoleInterface;
use Drupal\system\Entity\Action;
use Drupal\user\EntityOwnerInterface;
use Drupal\user\UserInterface;

/**
 * Implements hook_entity_insert().
 *
 * Subscribe the group manager.
 */
function og_entity_insert(EntityInterface $entity) {

  // Invalidate cache tags if new group content is created.
  og_invalidate_group_content_cache_tags($entity);
  if (!Og::isGroup($entity
    ->getEntityTypeId(), $entity
    ->bundle())) {

    // Not a group entity.
    return;
  }
  if (!$entity instanceof EntityOwnerInterface) {
    return;
  }
  $owner = $entity
    ->getOwner();
  if (empty($owner) || $owner
    ->isAnonymous()) {

    // User is anonymous, se we cannot set a membership for them.
    return;
  }

  // Other modules that implement hook_entity_insert() might already have
  // created a membership ahead of us.
  if (!Og::getMembership($entity, $entity
    ->getOwner(), OgMembershipInterface::ALL_STATES)) {
    $membership = Og::createMembership($entity, $entity
      ->getOwner());
    $membership
      ->save();
  }
}

/**
 * Implements hook_entity_update().
 */
function og_entity_update(EntityInterface $entity) {

  // Invalidate cache tags if a group or group content entity is updated.
  og_invalidate_group_content_cache_tags($entity);
}

/**
 * Implements hook_entity_predelete().
 */
function og_entity_predelete(EntityInterface $entity) {
  if (Og::isGroup($entity
    ->getEntityTypeId(), $entity
    ->bundle())) {

    // Register orphaned group content and user memberships for deletion, if
    // this option has been enabled.
    $config = \Drupal::config('og.settings');
    if ($config
      ->get('delete_orphans')) {
      $plugin_id = $config
        ->get('delete_orphans_plugin_id');

      /** @var \Drupal\og\OgDeleteOrphansInterface $plugin */
      $plugin = \Drupal::service('plugin.manager.og.delete_orphans')
        ->createInstance($plugin_id, []);
      $plugin
        ->register($entity);
    }

    // @todo Delete user roles.
    // @see https://github.com/amitaibu/og/issues/175
    // og_delete_user_roles_by_group($entity_type, $entity);
  }

  // If a user is being deleted, also delete its memberships.
  if ($entity instanceof UserInterface) {

    /** @var \Drupal\og\MembershipManagerInterface $membership_manager */
    $membership_manager = \Drupal::service('og.membership_manager');
    foreach ($membership_manager
      ->getMemberships($entity
      ->id(), []) as $membership) {
      $membership
        ->delete();
    }
  }
}

/**
 * Implements hook_entity_delete().
 */
function og_entity_delete(EntityInterface $entity) {

  // Invalidate cache tags after a group or group content entity is deleted.
  og_invalidate_group_content_cache_tags($entity);

  // Clear static caches after a group or group content entity is deleted.
  if (Og::isGroup($entity
    ->getEntityTypeId(), $entity
    ->bundle()) || Og::isGroupContent($entity
    ->getEntityTypeId(), $entity
    ->bundle())) {
    Og::invalidateCache();
  }

  // If a group content type is deleted, make sure to remove it from the list of
  // groups.
  if ($entity instanceof ConfigEntityBundleBase) {
    $bundle = $entity
      ->id();
    $entity_type_id = \Drupal::entityTypeManager()
      ->getDefinition($entity
      ->getEntityTypeId())
      ->getBundleOf();
    if (Og::isGroup($entity_type_id, $bundle)) {
      Og::groupTypeManager()
        ->removeGroup($entity_type_id, $bundle);
    }
  }
}

/**
 * Implements hook_entity_access().
 */
function og_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {

  // Grant access to view roles, so that they can be shown in listings.
  if ($entity instanceof OgRoleInterface && $operation == 'view') {
    return AccessResult::allowed();
  }

  // We only care about content entities that are groups or group content.
  if (!$entity instanceof ContentEntityInterface) {
    return AccessResult::neutral();
  }
  if ($operation == 'view') {
    return AccessResult::neutral();
  }
  $entity_type_id = $entity
    ->getEntityTypeId();
  $bundle_id = $entity
    ->bundle();
  $is_group_content = Og::isGroupContent($entity_type_id, $bundle_id);

  // If the entity is neither a group or group content, then we have no opinion.
  if (!Og::isGroup($entity_type_id, $bundle_id) && !$is_group_content) {
    return AccessResult::neutral();
  }

  // If the entity type is a group content type, but the entity is not
  // associated with any groups, we have no opinion.
  if ($is_group_content && \Drupal::service('og.membership_manager')
    ->getGroupCount($entity) === 0) {
    return AccessResult::neutral();
  }

  // If the user has the global permission to administer all groups, allow
  // access.
  if ($account
    ->hasPermission('administer organic groups')) {
    return AccessResult::allowed();
  }

  /** @var \Drupal\Core\Access\AccessResult $access */
  $access = \Drupal::service('og.access')
    ->userAccessEntityOperation($operation, $entity, $account);
  if ($access
    ->isAllowed()) {
    return $access;
  }
  if ($entity_type_id == 'node') {
    $node_access_strict = \Drupal::config('og.settings')
      ->get('node_access_strict');

    // Otherwise, ignore or deny based on whether strict node access is set.
    return AccessResult::forbiddenIf($node_access_strict);
  }
  return AccessResult::forbidden();
}

/**
 * Implements hook_entity_create_access().
 */
function og_entity_create_access(AccountInterface $account, array $context, $bundle) {
  $entity_type_id = $context['entity_type_id'];
  if (!Og::isGroupContent($entity_type_id, $bundle)) {

    // Not a group content.
    return AccessResult::neutral();
  }

  // A user with the global permission to administer all groups has full access.
  $access_result = AccessResult::allowedIfHasPermission($account, 'administer organic groups');
  if ($access_result
    ->isAllowed()) {
    return $access_result;
  }
  $node_access_strict = \Drupal::config('og.settings')
    ->get('node_access_strict');
  if ($entity_type_id == 'node' && !$node_access_strict && $account
    ->hasPermission("create {$bundle} content")) {

    // The user has the core permission and strict node access is not set.
    return AccessResult::neutral();
  }

  // We can't check if user has create permissions, as there is no group
  // context. However, we can check if there are any groups the user will be
  // able to select, and if not, we don't allow access but if there are,
  // AccessResult::neutral() will be returned in order to not override other
  // access results.
  // @see \Drupal\og\Plugin\EntityReferenceSelection\OgSelection::buildEntityQuery()
  $required = FALSE;
  $field_definitions = \Drupal::service('entity_field.manager')
    ->getFieldDefinitions($entity_type_id, $bundle);
  foreach ($field_definitions as $field_definition) {

    /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */
    if (!\Drupal::service('og.group_audience_helper')
      ->isGroupAudienceField($field_definition)) {
      continue;
    }
    $options = [
      'target_type' => $field_definition
        ->getFieldStorageDefinition()
        ->getSetting('target_type'),
      'handler' => $field_definition
        ->getSetting('handler'),
      'field_mode' => 'admin',
    ];

    /** @var \Drupal\Core\Entity\EntityReferenceSelection\SelectionPluginManager $handler */
    $handler = \Drupal::service('plugin.manager.entity_reference_selection');
    if ($handler
      ->getInstance($options)) {
      return AccessResult::neutral();
    }

    // Allow users to create content outside of groups, if none of the
    // audience fields is required.
    $required = $field_definition
      ->isRequired();
  }

  // Otherwise, ignore or deny based on whether strict entity access is set.
  return $required ? AccessResult::forbiddenIf($node_access_strict) : AccessResult::neutral();
}

/**
 * Implements hook_entity_bundle_field_info().
 *
 * Add a read only property to group entities as a group flag.
 */
function og_entity_bundle_field_info(EntityTypeInterface $entity_type, $bundle, array $base_field_definitions) {
  if (!Og::isGroup($entity_type
    ->id(), $bundle)) {

    // Not a group type.
    return NULL;
  }
  $fields = [];
  $fields['og_group'] = BaseFieldDefinition::create('og_group')
    ->setLabel(new TranslatableMarkup('OG Group'))
    ->setComputed(TRUE)
    ->setTranslatable(FALSE)
    ->setDefaultValue(TRUE)
    ->setReadOnly(TRUE);
  return $fields;
}

/**
 * Implements hook_entity_bundle_field_info_alter().
 *
 * Set the default field formatter of fields of type OG group.
 */
function og_entity_bundle_field_info_alter(&$fields, EntityTypeInterface $entity_type, $bundle) {
  if (!isset($fields['og_group'])) {

    // No OG group fields.
    return;
  }
  $fields['og_group']
    ->setDisplayOptions('view', [
    'weight' => 0,
    'type' => 'og_group_subscribe',
  ])
    ->setDisplayConfigurable('view', TRUE);
}

/**
 * Implements hook_field_formatter_info_alter().
 *
 * Allow OG audience fields to have entity reference formatters.
 */
function og_field_formatter_info_alter(array &$info) {
  foreach (array_keys($info) as $key) {
    if (!in_array('entity_reference', $info[$key]['field_types'])) {

      // Not an entity reference formatter.
      continue;
    }
    $info[$key]['field_types'][] = OgGroupAudienceHelperInterface::GROUP_REFERENCE;
  }
}

/**
 * Implements hook_field_widget_info_alter().
 */
function og_field_widget_info_alter(array &$info) {
  $info['options_buttons']['field_types'][] = OgGroupAudienceHelperInterface::GROUP_REFERENCE;
}

/**
 * Implements hook_entity_type_alter().
 *
 * Add link template to groups. We add it to all the entity types, and later on
 * return the correct access, depending if the bundle is indeed a group and
 * accessible. We do not filter here the entity type by groups, so whenever
 * GroupTypeManagerInterface::addGroup is called, it's enough to mark route to
 * be rebuilt via RouteBuilder::setRebuildNeeded.
 */
function og_entity_type_alter(array &$entity_types) {

  /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
  foreach ($entity_types as $entity_type_id => $entity_type) {
    $entity_type
      ->setLinkTemplate('og-admin-routes', "/group/{$entity_type_id}/{{$entity_type_id}}/admin");
  }
}

/**
 * Implements hook_theme().
 */
function og_theme($existing, $type, $theme, $path) {
  return [
    'og_member_count' => [
      'variables' => [
        'count' => 0,
        'membership_states' => [],
        'group' => NULL,
        'group_label' => NULL,
      ],
    ],
  ];
}

/**
 * Invalidates group content cache tags for the groups this entity belongs to.
 *
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   The group content entity that is being created, changed or deleted and is
 *   the direct cause for the need to invalidate cached group content.
 */
function og_invalidate_group_content_cache_tags(EntityInterface $entity) {

  // If group content is created or updated, invalidate the group content cache
  // tags for each of the groups this group content belongs to. This allows
  // group listings to be cached effectively. The cache tag format is
  // 'og-group-content:{group entity type}:{group entity id}'.
  $is_group_content = Og::isGroupContent($entity
    ->getEntityTypeId(), $entity
    ->bundle());
  if ($is_group_content) {

    /** @var \Drupal\og\MembershipManagerInterface $membership_manager */
    $membership_manager = \Drupal::service('og.membership_manager');
    $tags = [];

    // If the entity is a group content and we came here as an effect of an
    // update, check if any of the OG audience fields have been changed. This
    // means the group(s) of the entity changed and we should also invalidate
    // the tags of the old group(s).

    /** @var \Drupal\Core\Entity\FieldableEntityInterface $entity */
    $original = !empty($entity->original) ? $entity->original : NULL;
    if ($original) {

      /** @var \Drupal\og\OgGroupAudienceHelperInterface $group_audience_helper */
      $group_audience_helper = \Drupal::service('og.group_audience_helper');

      /** @var \Drupal\Core\Entity\FieldableEntityInterface $original */
      foreach ($group_audience_helper
        ->getAllGroupAudienceFields($entity
        ->getEntityTypeId(), $entity
        ->bundle()) as $field) {
        $field_name = $field
          ->getName();

        /** @var \Drupal\Core\Field\EntityReferenceFieldItemListInterface $original_field_item_list */
        $original_field_item_list = $original
          ->get($field_name);
        if (!$entity
          ->get($field_name)
          ->equals($original_field_item_list)) {
          foreach ($original_field_item_list
            ->referencedEntities() as $old_group) {
            $tags = Cache::mergeTags($tags, $old_group
              ->getCacheTagsToInvalidate());
          }
        }
      }
    }
    foreach ($membership_manager
      ->getGroups($entity) as $groups) {

      /** @var \Drupal\Core\Entity\ContentEntityInterface $group */
      foreach ($groups as $group) {
        $tags = Cache::mergeTags($tags, $group
          ->getCacheTagsToInvalidate());
      }
    }
    Cache::invalidateTags(Cache::buildTags('og-group-content', $tags));
  }
}

/**
 * Implements hook_ENTITY_TYPE_insert() for OgRole entities.
 */
function og_og_role_insert(OgRoleInterface $role) {

  // Create actions to add or remove the role, except for the required default
  // roles 'member' and 'non-member'. These cannot be added or removed.
  if ($role
    ->getRoleType() === OgRoleInterface::ROLE_TYPE_REQUIRED) {
    return;
  }

  // Skip creation of action plugins while config import is in progress.
  if ($role
    ->isSyncing()) {
    return;
  }
  $add_id = 'og_membership_add_single_role_action.' . $role
    ->getName();
  if (!Action::load($add_id)) {
    $action = Action::create([
      'id' => $add_id,
      'type' => 'og_membership',
      'label' => new TranslatableMarkup('Add the @label role to the selected members', [
        '@label' => $role
          ->getName(),
      ]),
      'configuration' => [
        'role_name' => $role
          ->getName(),
      ],
      'plugin' => 'og_membership_add_single_role_action',
    ]);
    $action
      ->trustData()
      ->save();
  }
  $remove_id = 'og_membership_remove_single_role_action.' . $role
    ->getName();
  if (!Action::load($remove_id)) {
    $action = Action::create([
      'id' => $remove_id,
      'type' => 'og_membership',
      'label' => new TranslatableMarkup('Remove the @label role from the selected members', [
        '@label' => $role
          ->getName(),
      ]),
      'configuration' => [
        'role_name' => $role
          ->getName(),
      ],
      'plugin' => 'og_membership_remove_single_role_action',
    ]);
    $action
      ->trustData()
      ->save();
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete() for OgRole entities.
 */
function og_og_role_delete(OgRoleInterface $role) {
  $role_name = $role
    ->getName();

  /** @var \Drupal\system\ActionConfigEntityInterface[] $actions */
  $actions = Action::loadMultiple([
    'og_membership_add_single_role_action.' . $role_name,
    'og_membership_remove_single_role_action.' . $role_name,
  ]);

  // Only remove the actions when the role name is not used by any other roles.
  foreach (OgRole::loadMultiple() as $role) {
    if ($role
      ->getName() === $role_name) {
      return;
    }
  }
  foreach ($actions as $action) {
    $action
      ->delete();
  }
}