<?php

/**
 * @file
 * Allows you to group users, content and other entities.
 */

use Drupal\Component\Utility\Html;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\group\Entity\GroupContent;
use Drupal\group\QueryAccess\EntityQueryAlter;
use Drupal\user\RoleInterface;
use Drupal\views\Plugin\views\query\QueryPluginBase;
use Drupal\views\Plugin\views\query\Sql;
use Drupal\views\ViewExecutable;

/**
 * Implements hook_help().
 */
function group_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'entity.group_type.content_plugins':
      return '<p>' . t('Entities that can be added to this group type.') . '</p>';
  }
}

/**
 * Implements hook_element_info_alter().
 */
function group_element_info_alter(array &$types) {
  // Attach our extra CSS for toolbar icons.
  if (isset($types['toolbar'])) {
    $types['toolbar']['#attached']['library'][] = 'group/toolbar';
  }
}

/**
 * Implements hook_config_translation_info_alter().
 */
function group_config_translation_info_alter(&$info) {
  $info['group_role']['class'] = 'Drupal\group\ConfigTranslation\GroupRoleMapper';
}

/**
 * Implements hook_theme().
 */
function group_theme() {
  return [
    'group' => [
      'render element' => 'elements',
    ],
    'group_content' => [
      'render element' => 'elements',
    ],
  ];
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function group_theme_suggestions_group(array $variables) {
  $suggestions = [];

  /** @var \Drupal\group\Entity\GroupInterface $group */
  $group = $variables['elements']['#group'];
  $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');

  $suggestions[] = 'group__' . $sanitized_view_mode;
  $suggestions[] = 'group__' . $group->bundle();
  $suggestions[] = 'group__' . $group->bundle() . '__' . $sanitized_view_mode;
  $suggestions[] = 'group__' . $group->id();
  $suggestions[] = 'group__' . $group->id() . '__' . $sanitized_view_mode;

  return $suggestions;
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function group_theme_suggestions_group_content(array $variables) {
  $suggestions = [];

  /** @var \Drupal\group\Entity\GroupContentInterface $group_content */
  $group_content = $variables['elements']['#group_content'];
  $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_');

  $suggestions[] = 'group_content__' . $sanitized_view_mode;
  $suggestions[] = 'group_content__' . $group_content->bundle();
  $suggestions[] = 'group_content__' . $group_content->bundle() . '__' . $sanitized_view_mode;
  $suggestions[] = 'group_content__' . $group_content->id();
  $suggestions[] = 'group_content__' . $group_content->id() . '__' . $sanitized_view_mode;

  return $suggestions;
}

/**
 * Prepares variables for the group template.
 *
 * Default template: group.html.twig
 *
 * @param array $variables
 *   - elements: An array of elements to display in view mode.
 *   - group: The group object.
 *   - view_mode: View mode; e.g., 'full', 'teaser', etc.
 */
function template_preprocess_group(&$variables) {
  /** @var \Drupal\group\Entity\GroupInterface $group */
  $group = $variables['elements']['#group'];

  $variables['group'] = $group;
  $variables['view_mode'] = $variables['elements']['#view_mode'];
  $variables['label'] = $group->label();
  $variables['url'] = $group->toUrl('canonical', ['language' => $group->language()]);

  $variables['attributes']['class'][] = 'group';
  $variables['attributes']['class'][] = Html::cleanCssIdentifier('group--' . $variables['view_mode']);
  $variables['attributes']['class'][] = Html::cleanCssIdentifier('group--' . $variables['group']->bundle());

  // See if we are rendering the group at its canonical route.
  $route_match = \Drupal::routeMatch();
  if ($route_match->getRouteName() == 'entity.group.canonical') {
    $page_group = $route_match->getParameter('group');
  }
  $is_page = (!empty($page_group) ? $page_group->id() == $group->id() : FALSE);
  $variables['page'] = $variables['view_mode'] == 'full' && $is_page;

  // Helpful $content variable for templates.
  $variables += ['content' => []];
  foreach (Element::children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }
}

/**
 * Prepares variables for the group content template.
 *
 * Default template: group-content.html.twig
 *
 * @param array $variables
 *   - elements: An array of elements to display in view mode.
 *   - group_content: The group content object.
 *   - view_mode: View mode; e.g., 'full', 'teaser', etc.
 */
function template_preprocess_group_content(&$variables) {
  /** @var \Drupal\group\Entity\GroupContentInterface $group_content */
  $group_content = $variables['elements']['#group_content'];

  $variables['group_content'] = $group_content;
  $variables['view_mode'] = $variables['elements']['#view_mode'];
  $variables['label'] = $group_content->label();
  $variables['url'] = $group_content->toUrl('canonical', ['language' => $group_content->language()]);

  $variables['attributes']['class'][] = 'group-content';
  $variables['attributes']['class'][] = Html::cleanCssIdentifier('group-content--' . $variables['view_mode']);

  // See if we are rendering the group at its canonical route.
  $route_match = \Drupal::routeMatch();
  if ($route_match->getRouteName() == 'entity.group_content.canonical') {
    $page_group_content = $route_match->getParameter('group_content');
  }
  $is_page = (!empty($page_group_content) ? $page_group_content->id() == $group_content->id() : FALSE);
  $variables['page'] = $variables['view_mode'] == 'full' && $is_page;

  // Helpful $content variable for templates.
  $variables += ['content' => []];
  foreach (Element::children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }
}

/**
 * Implements hook_rebuild().
 */
function group_rebuild() {
  _group_content_enabler_manager()->installEnforced();

  /** @var \Drupal\group\Entity\Storage\GroupRoleStorageInterface $storage */
  $storage = \Drupal::entityTypeManager()->getStorage('group_role');
  $storage->createInternal();
  $storage->createSynchronized();
}

/**
 * Implements hook_modules_installed().
 */
function group_modules_installed($modules) {
  // Only create config objects while config import is not in progress.
  if (!\Drupal::isConfigSyncing()) {
    _group_content_enabler_manager()->installEnforced();

    /** @var \Drupal\group\Entity\Storage\GroupRoleStorageInterface $storage */
    $storage = \Drupal::entityTypeManager()->getStorage('group_role');
    $storage->createInternal();
    $storage->createSynchronized();
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 *
 * @param array $form
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 * @param string $form_id
 */
function group_form_block_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if (isset($form['visibility']['group_type'])) {
    $form['visibility_tabs']['#attached']['library'][] = 'group/block';
    $form['visibility']['group_type']['#title'] = t('Group types');
    $form['visibility']['group_type']['negate']['#type'] = 'value';
    $form['visibility']['group_type']['negate']['#title_display'] = 'invisible';
    $form['visibility']['group_type']['negate']['#value'] = $form['visibility']['group_type']['negate']['#default_value'];
  }
}

/**
 * Implements hook_module_implements_alter().
 */
function group_module_implements_alter(&$implementations, $hook) {
  // Make sure our query alters happen at the very last moment.
  if (in_array($hook, ['query_entity_query_alter', 'views_query_alter'], TRUE)) {
    // Move hook to the end of the list.
    $group = $implementations['group'];
    unset($implementations['group']);
    $implementations['group'] = $group;
  }
}

/**
 * Implements hook_query_TAG_alter().
 */
function group_query_entity_query_alter(SelectInterface $query) {
  $entity_type_id = $query->getMetaData('entity_type');
  if ($query->hasTag($entity_type_id . '_access')) {
    $entity_type_manager = \Drupal::entityTypeManager();
    $entity_type = $entity_type_manager->getDefinition($entity_type_id);

    \Drupal::service('class_resolver')
      ->getInstanceFromDefinition(EntityQueryAlter::class)
      ->alter($query, $entity_type);
  }
}

/**
 * Implements hook_query_TAG_alter().
 */
function group_query_views_entity_query_alter(SelectInterface $query) {
  $entity_type_id = $query->getMetaData('entity_type');
  $query->addTag($entity_type_id . '_access');
  group_query_entity_query_alter($query);
}

/**
 * Implements hook_views_query_alter().
 */
function group_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
  if ($query instanceof Sql && empty($query->options['disable_sql_rewrite'])) {
    $table_info = $query->getEntityTableInfo();
    $base_table = reset($table_info);
    if (empty($base_table['entity_type']) || $base_table['relationship_id'] != 'none') {
      return;
    }

    // Add a custom tag so we don't trigger all other 'entity_query' tag alters.
    $entity_type_id = $base_table['entity_type'];
    $query->addTag('views_entity_query');

    // Build the Views query already so we can access the DB query.
    $query->build($view);
    $view->built = TRUE;

    // Add metadata to the DB query.
    $query = $view->build_info['query'];
    $count_query = $view->build_info['count_query'];
    $query->addMetaData('entity_type', $entity_type_id);
    $count_query->addMetaData('entity_type', $entity_type_id);
  }
}

/**
 * Implements hook_entity_access().
 */
function group_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
  // Some modules, including the code in \Drupal\node\NodeForm::access() may
  // check for 'view', 'update' or 'delete' access on new entities, even though
  // that makes little sense. We need to account for it to avoid crashes because
  // we would otherwise query the DB with a non-existent node ID.
  if ($entity->isNew()) {
    return AccessResult::neutral();
  }

  /** @var \Drupal\group\Plugin\GroupContentEnablerManagerInterface $plugin_manager */
  $plugin_manager = \Drupal::service('plugin.manager.group_content_enabler');

  // Find all of the group content plugins that define access.
  $plugin_ids = $plugin_manager->getPluginIdsByEntityTypeAccess($entity->getEntityTypeId());
  if (empty($plugin_ids)) {
    return AccessResult::neutral();
  }

  // If any new group content entity is added using any of the retrieved
  // plugins, it might change access.
  $plugin_cache_tags = [];
  foreach ($plugin_ids as $plugin_id) {
    $plugin_cache_tags[] = "group_content_list:plugin:$plugin_id";
  }

  // Load all of the group content for this entity.
  $group_contents = \Drupal::entityTypeManager()
    ->getStorage('group_content')
    ->loadByEntity($entity);

  // If the entity does not belong to any group, we have nothing to say.
  //
  // @todo There is a slight performance boost to be had here. If we have a
  // plugin that adds content without access and one that adds the same content
  // with access, then this check would fail for content that is only added
  // using the non-access plugin. The loop over the plugin IDs below would then
  // not return anything and we would still end up with a neutral result, albeit
  // with the user.group_permissions context and slightly worse performance. We
  // can fix this by only loading those group content entities that use the
  // plugins retrieved above.
  if (empty($group_contents)) {
    return AccessResult::neutral()->addCacheTags($plugin_cache_tags);
  }

  $access = AccessResult::neutral();
  foreach ($plugin_ids as $plugin_id) {
    // For backwards compatibility reasons, if the group content enabler plugin
    // used by the group content type does not specify an access handler, we do
    // not check access for that group content type. In 8.2.x all group content
    // enabler plugins will get a permission handler by default, so this check
    // can be safely removed then.
    if (!$plugin_manager->hasHandler($plugin_id, 'access')) {
      continue;
    }

    $handler = $plugin_manager->getAccessControlHandler($plugin_id);
    $access = $access->orIf($handler->entityAccess($entity, $operation, $account, TRUE));
  }

  return $access
    ->addCacheTags($plugin_cache_tags)
    ->addCacheContexts(['user.group_permissions']);
}

/**
 * Implements hook_entity_delete().
 */
function group_entity_delete(EntityInterface $entity) {
  if ($entity instanceof ContentEntityInterface) {
    if ($group_contents = GroupContent::loadByEntity($entity)) {
      /** @var \Drupal\group\Entity\GroupContent $group_content */
      foreach ($group_contents as $group_content) {
        $group_content->delete();
      }
    }
  }
}

/**
 * Implements hook_entity_field_access().
 *
 * {@inheritdoc}
 *
 * @todo Move this to a form controller so we can hide the field if it has no
 *       options available to it?
 */
function group_entity_field_access($operation, FieldDefinitionInterface $field_definition, AccountInterface $account, FieldItemListInterface $items = NULL) {
  // Can't retrieve an entity from an empty item list.
  if (!isset($items)) {
    return AccessResult::neutral();
  }

  // We only care about the group_roles field when on a form.
  if ($field_definition->getName() != 'group_roles' || $operation !== 'edit') {
    return AccessResult::neutral();
  }

  // We only care if it is attached to a group content entity.
  if ($items->getEntity()->getEntityTypeId() != 'group_content') {
    return AccessResult::neutral();
  }

  /** @var \Drupal\group\Entity\GroupContentInterface $group_content */
  $group_content = $items->getEntity();

  // We only care if the group content entity is a group membership.
  if ($group_content->getContentPlugin()->getPluginId() != 'group_membership') {
    return AccessResult::neutral();
  }

  // Now that we know we're dealing with a group_roles field on a group
  // membership form, we need to check whether the group membership belongs to a
  // group yet. If not, we can't check for access and should always hide it.
  if (!$group = $group_content->getGroup()) {
    return AccessResult::forbidden();
  }

  // Only group administrators should be able to change the membership roles.
  return AccessResult::forbiddenIf(!$group->hasPermission('administer members', $account));
}

/**
 * Implements hook_user_cancel_methods_alter().
 */
function group_user_cancel_methods_alter(&$methods) {
  $methods['user_cancel_block']['title'] = t('Disable the account and keep its content and groups.');
  $methods['user_cancel_block']['description'] .= ' ' . t('Groups that were created by you will still list you as the owner.');
  $methods['user_cancel_block_unpublish']['title'] .= ' ' . t('Does not affect groups.');
  $methods['user_cancel_block_unpublish']['description'] .= ' ' . t('Groups that were created by you will remain visible.');
  $methods['user_cancel_reassign']['title'] .= ' ' . t('Reassign its groups to the super administrator.');
  $methods['user_cancel_reassign']['description'] .= ' ' . t('All of your groups will be assigned to the super administrator.');
  $methods['user_cancel_delete']['title'] = t('Delete the account, its content and groups.');
  $methods['user_cancel_delete']['description'] .= ' ' . t('This includes groups that were created by you, including all of their content relationships!');
}

/**
 * Implements hook_user_cancel().
 */
function group_user_cancel($edit, $account, $method) {
  // Reassign all groups to the super user.
  if ($method == 'user_cancel_reassign') {
    $storage = \Drupal::entityTypeManager()->getStorage('group');
    $gids = $storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('uid', $account->id())
      ->execute();

    // Run this as a batch if there are more than 10 groups.
    if (count($gids) > 10) {
      batch_set(['operations' => [['_group_mass_reassign_to_super_user', [$gids]]]]);
    }
    // Run it immediately if not.
    else {
      foreach ($storage->loadMultiple($gids) as $group) {
        /** @var \Drupal\group\Entity\GroupInterface $group */
        $group->set('uid', 1);
        $storage->save($group);
      }
    }
  }
}

/**
 * Implements callback_batch_operation().
 *
 * Mass reassigns ownership of groups to the super user.
 *
 * @param int[] $ids
 *   An array of group IDs.
 */
function _group_mass_reassign_to_super_user(array $ids, &$context) {
  if (!isset($context['sandbox']['progress'])) {
    $context['sandbox']['progress'] = 0;
    $context['sandbox']['max'] = count($ids);
    $context['sandbox']['ids'] = $ids;
  }

  // Try to update 10 groups at a time.
  $ids = array_slice($context['sandbox']['ids'], $context['sandbox']['progress'], 10);

  $storage = \Drupal::entityTypeManager()->getStorage('group');
  foreach ($storage->loadMultiple($ids) as $group) {
    /** @var \Drupal\group\Entity\GroupInterface $group */
    $group->set('uid', 1);
    $storage->save($group);
    $context['sandbox']['progress']++;
  }

  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
    $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function group_user_update(AccountInterface $account) {
  // If a user's roles change, we need to reset their group roles cache.
  $new = array_unique($account->getRoles());
  $old = array_unique($account->original->getRoles());
  sort($new);
  sort($old);

  if ($new != $old) {
    /** @var \Drupal\group\Entity\Storage\GroupRoleStorageInterface $storage */
    $storage = \Drupal::entityTypeManager()->getStorage('group_role');
    $storage->resetUserGroupRoleCache($account);
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function group_user_delete(EntityInterface $account) {
  // If a user is deleted, we delete all of their groups too.
  $storage = \Drupal::entityTypeManager()->getStorage('group');
  if ($groups = $storage->loadByProperties(['uid' => $account->id()])) {
    $storage->delete($groups);
  }
}

/**
 * Implements hook_ENTITY_TYPE_insert().
 */
function group_user_role_insert(RoleInterface $role) {
  // Only create config objects while config import is not in progress.
  if (!\Drupal::isConfigSyncing()) {
    /** @var \Drupal\group\Entity\Storage\GroupRoleStorageInterface $storage */
    $storage = \Drupal::entityTypeManager()->getStorage('group_role');
    $storage->createSynchronized(NULL, [$role->id()]);
  }
}

/**
 * Implements hook_ENTITY_TYPE_update().
 */
function group_user_role_update(RoleInterface $role) {
  /** @var \Drupal\User\RoleInterface $original */
  $original = $role->original;

  // Update the group roles if the user role label changed.
  if ($role->label() != $original->label()) {
    /** @var \Drupal\group\Entity\Storage\GroupRoleStorageInterface $storage */
    $storage = \Drupal::entityTypeManager()->getStorage('group_role');
    $storage->updateSynchronizedLabels($role);
  }
}

/**
 * Implements hook_form_alter().
 */
function group_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  // GroupContentController::createForm() tends to load entity forms for adding
  // entities to a group. We need to add or alter the submit handlers of those
  // forms for the process to work properly.
  if ($form_state->has('group_wizard') && $form_state->get('group_wizard_id') == 'group_entity') {
    if ($wizard = $form_state->get('group_wizard')) {
      $store = \Drupal::service('tempstore.private')->get($form_state->get('group_wizard_id'));
      $store_id = $form_state->get('store_id');

      // Bail out if we are on step 2 of the wizard. We only want to alter the
      // submit handlers for the first step or if we are not in a wizard.
      if ($store->get("$store_id:step") === 2) {
        return;
      }
    }

    foreach (Element::children($form['actions']) as $name) {
      // Remove preview button as it redirects back to the wrong form.
      if ($name == 'preview') {
        unset($form['actions'][$name]);
        continue;
      }

      // Skip buttons without submit handlers.
      if (empty($form['actions'][$name]['#submit'])) {
        continue;
      }

      // Skip buttons that do not properly build and save an entity.
      $submit = $form['actions'][$name]['#submit'];
      if (!in_array('::submitForm', $submit) || !in_array('::save', $submit)) {
        continue;
      }

      // If we are using the wizard, we need to substitute the entity save
      // handler in order to add the entity to the private temp store.
      if ($wizard) {
        foreach ($submit as $key => $handler) {
          if ($handler == '::save') {
            $form['actions'][$name]['#submit'][$key] = 'group_content_wizard_store';
          }
        }
      }
      // Otherwise, we can simply add our submit handler and be done with it.
      else {
        $form['actions'][$name]['#submit'][] = 'group_content_entity_submit';
      }
    }

    // If we are using the wizard, we also add a cancel button to step 1.
    if ($wizard) {
      $form['actions']['cancel'] = [
        '#type' => 'submit',
        '#value' => t('Cancel'),
        '#submit' => ['group_content_wizard_cancel'],
        '#limit_validation_errors' => [],
      ];
    }
  }
}

/**
 * Stores a content entity from the wizard step 1 in the temp store.
 *
 * @see group_form_alter()
 * @see \Drupal\group\Entity\Controller\GroupContentController::createForm()
 */
function group_content_wizard_store($form, FormStateInterface $form_state) {
  /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
  $entity = $form_state->getFormObject()->getEntity();

  // Store the unsaved entity in the temp store.
  $store = \Drupal::service('tempstore.private')->get($form_state->get('group_wizard_id'));
  $store_id = $form_state->get('store_id');
  $store->set("$store_id:entity", $entity);
  $store->set("$store_id:step", 2);

  // Disable any URL-based redirect until the final step.
  $request = \Drupal::service('request_stack')->getCurrentRequest();
  $form_state->setRedirect('<current>', [], ['query' => $request->query->all()]);
  $request->query->remove('destination');
}

/**
 * Cancels the wizard for group content creation.
 *
 * @see group_form_alter()
 * @see \Drupal\group\Entity\Controller\GroupContentController::createForm()
 */
function group_content_wizard_cancel($form, FormStateInterface $form_state) {
  $store = \Drupal::service('tempstore.private')->get($form_state->get('group_wizard_id'));
  $store_id = $form_state->get('store_id');
  $store->delete("$store_id:entity");
  $store->delete("$store_id:step");

  /** @var \Drupal\group\Entity\GroupInterface $group */
  $group = $form_state->get('group');

  // Redirect to the group page if no destination was set in the URL.
  $form_state->setRedirect('entity.group.canonical', ['group' => $group->id()]);
}

/**
 * Adds a newly saved entity to a group.
 *
 * @see group_form_alter()
 * @see \Drupal\group\Entity\Controller\GroupContentController::createForm()
 */
function group_content_entity_submit($form, FormStateInterface $form_state) {
  /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
  $entity = $form_state->getFormObject()->getEntity();

  /** @var \Drupal\group\Entity\GroupInterface $group */
  $group = $form_state->get('group');
  $group->addContent($entity, $form_state->get('group_content_enabler'));

  // This submit handler is only called when creating new content within a group
  // without using the 2-step wizard. We can therefore safely assume the user
  // wants to see the entity itself and not the relationship (group content).
  // This only applies if there was no destination GET parameter set in the URL.
  if ($entity->access('view')) {
    $form_state->setRedirectUrl($entity->toUrl());
  }
  elseif ($group->access('view')) {
    $form_state->setRedirectUrl($group->toUrl());
  }
  else {
    $form_state->setRedirect('<front>');
  }
}

/**
 * @defgroup group_access Group access rights
 * @{
 * The group access system determines who can do what in which groups.
 *
 * Each group type can specify an unlimited amount of group roles. Just like
 * user roles in the global scope, a group role can be assigned specific
 * permissions. Users who are then given such group roles will be able to
 * perform any action their permissions allow them.
 *
 * There are three categories of users who are eligible to receive group roles:
 * - Anonymous: These users do not have a site account and will only receive the
 *   'anonymous' group role, which typically grants them a very limited set of
 *   permissions.
 * - Outsider: These users have a site account, but are not yet a member of the
 *   group in question. They will only receive the 'outsider' group role. This
 *   role is usually more permissive than 'anonymous'.
 * - Member: These users have a site account and are part of the group. They
 *   will automatically receive the 'member' role but can be assigned any other
 *   user defined group role. Common examples are: Editor, Admin, etc.
 *
 * Please keep in mind that the 'anonymous', 'outsider' and 'member' role will
 * always be assigned because they're assumed roles based on the user's account
 * status.
 */

// Nothing to see here yet, our own access hook implementations will go here.

/**
 * @} End of "defgroup group_access".
 */

/**
 * Gets the group content enabler plugin manager.
 *
 * @return \Drupal\group\Plugin\GroupContentEnablerManagerInterface
 *   The group content enabler plugin manager.
 *
 * @internal Try to properly inject the service when possible.
 */
function _group_content_enabler_manager() {
  return \Drupal::service('plugin.manager.group_content_enabler');
}

/**
 * Gets the group role synchronizer service.
 *
 * @return \Drupal\group\GroupRoleSynchronizerInterface
 *   The group role synchronizer service.
 *
 * @internal Try to properly inject the service when possible.
 */
function _group_role_synchronizer() {
  return \Drupal::service('group_role.synchronizer');
}

/**
 * Implements hook_ENTITY_TYPE_presave().
 *
 * @todo Remove in 8.2.x if it still only fixes cache contexts by then.
 */
function group_view_presave(EntityInterface $view) {
  $displays = $view->get('display');
  $update = FALSE;

  // Loop over all displays and replace the group_membership.roles.permissions
  // cache context with its safe user.group_permissions counterpart.
  foreach ($displays as $display_name => &$display) {
    $cache_metadata = &$display['cache_metadata'];
    if (isset($cache_metadata['contexts'])) {
      // Remove the deleted cache context and add the updated metadata.
      $key = array_search('group_membership.roles.permissions', $cache_metadata['contexts'], TRUE);
      if ($key !== FALSE) {
        $update = TRUE;
        unset($cache_metadata['contexts'][$key]);

        $cache_metadata['max-age'] = CacheBackendInterface::CACHE_PERMANENT;
        $cache_metadata['contexts'][] = 'route.group';
        $cache_metadata['contexts'][] = 'user.group_permissions';
      }
    }
  }

  if ($update) {
    $view->set('display', $displays);
  }
}
