You are here

apigee_edge_teams.module in Apigee Edge 8

Copyright 2018 Google Inc.

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.

This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.

File

modules/apigee_edge_teams/apigee_edge_teams.module
View source
<?php

/**
 * @file
 * Copyright 2018 Google Inc.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA 02110-1301, USA.
 */
use Drupal\apigee_edge\Exception\DeveloperDoesNotExistException;
use Drupal\apigee_edge_teams\Entity\TeamInterface;
use Drupal\apigee_edge_teams\Entity\TeamInvitationInterface;
use Drupal\apigee_edge_teams\Entity\TeamRoleInterface;
use Drupal\apigee_edge_teams\EventSubscriber\TeamInactiveStatusSubscriber;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;

/**
 * Implements hook_module_implements_alter().
 */
function apigee_edge_teams_module_implements_alter(&$implementations, $hook) {
  if (\Drupal::moduleHandler()
    ->moduleExists('devel') && $hook === 'entity_type_alter') {

    // Move apigee_edge_teams_entity_type_alter() to the end of the list if
    // Devel module is enabled.
    // @see devel_entity_type_alter()
    $group = $implementations['apigee_edge_teams'];
    unset($implementations['apigee_edge_teams']);
    $implementations['apigee_edge_teams'] = $group;
  }
}

/**
 * Implements hook_entity_type_alter().
 */
function apigee_edge_teams_entity_type_alter(array &$entity_types) {

  // Fixes Team App entity routes generated by the Devel module.
  // @see devel_entity_type_alter()
  // @see \Drupal\apigee_edge_teams\Routing\TeamAppDevelRouteFixerSubscriber
  if (\Drupal::moduleHandler()
    ->moduleExists('devel')) {

    /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
    $entity_type =& $entity_types['team_app'];
    if ($entity_type
      ->hasLinkTemplate('canonical')) {
      $canonical_link = $entity_type
        ->getLinkTemplate('canonical');
      $entity_type
        ->setLinkTemplate('devel-render', "{$canonical_link}/devel-render");
      $entity_type
        ->setLinkTemplate('devel-definition', "{$canonical_link}/devel-definition");
      if ($entity_type
        ->hasLinkTemplate('edit-form')) {
        $entity_type
          ->setLinkTemplate('devel-load', "{$canonical_link}/devel-load");
      }
    }
  }
}

/**
 * Gets the title of team listing page.
 *
 * @return \Drupal\Core\StringTranslation\TranslatableMarkup
 *   The title of the page.
 */
function apigee_edge_teams_team_listing_page_title() : TranslatableMarkup {
  $args['@teams'] = \Drupal::entityTypeManager()
    ->getDefinition('team')
    ->getCollectionLabel();
  $title = t('@teams', $args);

  // Modules and themes can alter the title.
  \Drupal::moduleHandler()
    ->alter('apigee_edge_teams_team_listing_page_title', $title);
  return $title;
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function apigee_edge_teams_form_entity_form_display_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  if ($form['#entity_type'] === 'team_app') {
    $form['#validate'][] = '_apigee_edge_teams_team_app_entity_form_display_edit_form_validate';
  }
}

/**
 * Extra validation for the entity_form_display.edit form of team apps.
 *
 * This makes sure that fields marked as 'required' can't be disabled.
 *
 * @param array $form
 *   Form array.
 * @param Drupal\Core\Form\FormStateInterface $form_state
 *   Form state.
 */
function _apigee_edge_teams_team_app_entity_form_display_edit_form_validate(array &$form, FormStateInterface $form_state) {
  $required = \Drupal::config('apigee_edge_teams.team_app_settings')
    ->get('required_base_fields');
  foreach ($form_state
    ->getValue('fields') as $field_name => $data) {
    if (in_array($field_name, $required) && $data['region'] === 'hidden') {
      $form_state
        ->setError($form['fields'][$field_name], t('%field-name is required.', [
        '%field-name' => $form['fields'][$field_name]['human_name']['#plain_text'],
      ]));
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function apigee_edge_teams_user_delete(EntityInterface $entity) {

  /** @var \Drupal\user\UserInterface $entity */

  /** @var \Drupal\apigee_edge_teams\Entity\Storage\TeamMemberRoleStorageInterface $team_member_role_storage */
  $team_member_role_storage = \Drupal::entityTypeManager()
    ->getStorage('team_member_role');

  // When a user gets deleted then its developer account also gets deleted
  // from Apigee Edge which removes its (team) company memberships.
  // We must delete this user's team roles from Drupal as well.
  foreach ($team_member_role_storage
    ->loadByDeveloper($entity) as $team_member_roles_in_team) {
    try {
      $team_member_roles_in_team
        ->delete();
    } catch (EntityStorageException $e) {
      \Drupal::logger('apigee_edge_teams')
        ->critical("Integrity check: Failed to remove %developer team member's roles in %team team when its Drupal user got deleted.", [
        '%developer' => $entity
          ->getEmail(),
        '%team' => $team_member_roles_in_team
          ->getTeam()
          ->id(),
      ]);
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function apigee_edge_teams_developer_delete(EntityInterface $entity) {

  /** @var \Drupal\apigee_edge\Entity\DeveloperInterface $entity */

  /** @var \Drupal\apigee_edge_teams\CompanyMembershipObjectCacheInterface $cache */
  $cache = \Drupal::service('apigee_edge_teams.cache.company_membership_object');

  // Remove all company membership object cache entries that contained the
  // removed developer.
  $cache
    ->invalidateMemberships([
    "developer:{$entity->getEmail()}",
  ]);
}

/**
 * Implements hook_ENTITY_TYPE_delete().
 */
function apigee_edge_teams_team_delete(EntityInterface $entity) {

  // Delete all invitations from this team.

  /** @var \Drupal\apigee_edge_teams\Entity\Storage\TeamStorageInterface $storage */
  $storage = \Drupal::entityTypeManager()
    ->getStorage('team_invitation');
  if ($invitations = $storage
    ->loadByProperties([
    'team' => $entity
      ->id(),
  ])) {
    $storage
      ->delete($invitations);
  }
}

/**
 * Implements hook_system_breadcrumb_alter().
 */
function apigee_edge_teams_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {
  if ($route_match
    ->getRouteName() === 'entity.team.add_form') {
    $team_entity_def = \Drupal::entityTypeManager()
      ->getDefinition('team');
    $breadcrumb
      ->addLink(Link::createFromRoute($team_entity_def
      ->getPluralLabel(), 'entity.team.collection'));
  }
  elseif ($route_match
    ->getRouteName() === 'entity.team.add_members') {

    /** @var \Drupal\apigee_edge_teams\Entity\TeamInterface $team */
    $team = $route_match
      ->getParameter('team');
    $breadcrumb
      ->addLink($team
      ->toLink(t('Members'), 'members'));
  }
  elseif ($route_match
    ->getRouteName() === 'entity.team_app.add_form_for_team') {
    $team_app_entity_def = \Drupal::entityTypeManager()
      ->getDefinition('team_app');

    /** @var \Drupal\apigee_edge_teams\Entity\TeamInterface $team */
    $team = $route_match
      ->getParameter('team');
    $breadcrumb
      ->addLink(Link::createFromRoute($team_app_entity_def
      ->getPluralLabel(), 'entity.team_app.collection_by_team', [
      'team' => $team
        ->id(),
    ]));
  }
}

/**
 * Implements hook_preprocess().
 */
function apigee_edge_teams_preprocess(&$variables, $hook) {
  if (!in_array($hook, [
    'menu_local_action',
    'menu_local_task',
  ])) {
    return;
  }

  /** @var \Drupal\Core\Url $url */
  $url = $variables['link']['#url'];
  if (!in_array($url
    ->getRouteName(), TeamInactiveStatusSubscriber::getDisabledRoutes())) {
    return;
  }
  $team = \Drupal::routeMatch()
    ->getParameter('team');
  if (!$team || $team
    ->getStatus() !== TeamInterface::STATUS_INACTIVE) {
    return;
  }

  // If a team is inactive, for the local tasks and local actions, set the url
  // to <none>.
  $variables['link']['#url'] = Url::fromRoute('<none>', [], [
    'absolute' => TRUE,
  ]);

  // Add a title attribute.
  $variables['link']['#options']['attributes']['title'] = t('This @team is inactive.', [
    '@team' => \Drupal::entityTypeManager()
      ->getDefinition('team')
      ->getSingularLabel(),
  ]);

  // Add a disabled class.
  $variables['link']['#options']['attributes']['class'][] = 'team-disabled-action';
  $variables['#attached']['library'][] = 'apigee_edge_teams/disabled_action';
}

/**
 * Implements hook_entity_operation_alter().
 */
function apigee_edge_teams_entity_operation_alter(array &$operations, EntityInterface $entity) {
  foreach ($operations as $key => $operation) {
    if (!in_array($operation['url']
      ->getRouteName(), TeamInactiveStatusSubscriber::getDisabledRoutes())) {
      continue;
    }
    $team = \Drupal::routeMatch()
      ->getParameter('team');
    if (!$team || $team
      ->getStatus() !== TeamInterface::STATUS_INACTIVE) {
      continue;
    }

    // Remove the entity operation if the team is inactive.
    unset($operations[$key]);
  }
}

/**
 * Implements hook_ENTITY_TYPE_access().
 *
 * Grant "view" and "view label" access to team members based on their
 * teams' API Product access.
 */
function apigee_edge_teams_api_product_access(EntityInterface $entity, $operation, AccountInterface $account) {

  /** @var \Drupal\apigee_edge\Entity\ApiProductInterface $entity */

  // The "assign" in not in this list, because it is handled by team API Product
  // access manager service directly. Team members should not be able to
  // assign API products to their developer apps just because they have access
  // to do that when they are creating team app for a team.
  if (!in_array($operation, [
    'view',
    'view label',
  ])) {
    return AccessResult::neutral(sprintf('%s is not supported by %s.', $operation, __FUNCTION__));
  }
  if ($account
    ->isAnonymous()) {
    return AccessResult::neutral('Anonymous user can not be member of a team.');
  }

  /** @var \Drupal\apigee_edge_teams\TeamMemberApiProductAccessHandlerInterface $access_checker */
  $access_checker = \Drupal::service('apigee_edge_teams.team_member_api_product_access_handler');

  /** @var \Drupal\apigee_edge_teams\TeamMembershipManagerInterface $team_membership_manager */
  $team_membership_manager = \Drupal::service('apigee_edge_teams.team_membership_manager');
  try {
    $developer_team_ids = $team_membership_manager
      ->getTeams($account
      ->getEmail());
  } catch (DeveloperDoesNotExistException $e) {
    return AccessResult::neutral($e
      ->getMessage());
  }
  if (empty($developer_team_ids)) {
    $result = AccessResult::neutral("{$account->getEmail()} is not member of any team.");

    // If developer's team membership changes access must be re-evaluated.
    // @see \Drupal\apigee_edge_teams\TeamMembershipManager

    /** @var \Drupal\apigee_edge\Entity\Storage\DeveloperStorageInterface $developer_storage */
    $developer_storage = \Drupal::entityTypeManager()
      ->getStorage('developer');
    $developer = $developer_storage
      ->load($account
      ->getEmail());
    if ($developer) {
      $result
        ->addCacheableDependency($developer);
    }
  }
  else {

    /** @var \Drupal\apigee_edge_teams\Entity\Storage\TeamStorageInterface $team_storage */
    $team_storage = \Drupal::entityTypeManager()
      ->getStorage('team');

    /** @var \Drupal\apigee_edge_teams\Entity\TeamInterface $team */
    $teams = $team_storage
      ->loadMultiple($developer_team_ids);
    foreach ($teams as $team) {
      $result = $access_checker
        ->access($entity, $operation, $team, $account, TRUE);
      if ($result
        ->isAllowed()) {
        break;
      }
    }
  }

  // $result is always defined.
  return $result;
}

/**
 * Implements hook_help().
 */
function apigee_edge_teams_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {
    case 'entity.team_role.collection':
      $member_role = \Drupal::entityTypeManager()
        ->getStorage('team_role')
        ->load(TeamRoleInterface::TEAM_MEMBER_ROLE);
      return '<p>' . t('A role defines a group of users that have certain privileges. These privileges are defined on the <a href=":permissions">Permissions page</a>. Here you can define the names of the team roles on your site. Users who are part of a team have the :member team role, plus any other team roles granted to their user account.', [
        ':permissions' => Url::fromRoute('apigee_edge_teams.settings.team.permissions')
          ->toString(),
        ':member' => $member_role
          ->label(),
      ]) . '</p>';
  }
}

/**
 * Implements hook_mail().
 */
function apigee_edge_teams_mail($key, &$message, $params) {
  $token_service = \Drupal::token();
  $config = \Drupal::config('apigee_edge_teams.team_settings');
  switch ($key) {
    case 'team_invitation_created':
      $template = empty($params['user']) ? 'new' : 'existing';
      $message['subject'] = PlainTextOutput::renderFromHtml($token_service
        ->replace($config
        ->get("team_invitation_email_{$template}.subject"), $params));
      $message['body'][] = $token_service
        ->replace($config
        ->get("team_invitation_email_{$template}.body"), $params);
      break;
  }
}

/**
 * Implements hook_cron().
 */
function apigee_edge_teams_cron() {

  /** @var \Drupal\apigee_edge_teams\Entity\Storage\TeamInvitationStorageInterface $storage */
  $storage = Drupal::entityTypeManager()
    ->getStorage('team_invitation');
  $team_invitations = $storage
    ->getInvitationsToExpire();
  if (!count($team_invitations)) {
    return;
  }

  // Update status for expired invitations.
  foreach ($team_invitations as $team_invitation) {
    $team_invitation
      ->setStatus(TeamInvitationInterface::STATUS_EXPIRED)
      ->save();
  }
}