You are here

apigee_edge.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

apigee_edge.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.
 */

/**
 * Main module file for Apigee Edge.
 */
use Apigee\Edge\Api\Management\Entity\AppCredentialInterface;
use Apigee\Edge\Api\Management\Serializer\AppCredentialSerializer;
use Apigee\Edge\Exception\ApiException;
use Apigee\Edge\Exception\ClientErrorException;
use Apigee\Edge\Structure\CredentialProduct;
use Drupal\apigee_edge\Element\StatusPropertyElement;
use Drupal\apigee_edge\Entity\ApiProduct;
use Drupal\apigee_edge\Entity\AppInterface;
use Drupal\apigee_edge\Entity\Developer;
use Drupal\apigee_edge\Exception\DeveloperUpdateFailedException;
use Drupal\apigee_edge\Exception\UserDeveloperConversionNoStorageFormatterFoundException;
use Drupal\apigee_edge\Exception\UserDeveloperConversionUserFieldDoesNotExistException;
use Drupal\apigee_edge\Form\DeveloperSettingsForm;
use Drupal\apigee_edge\JobExecutor;
use Drupal\apigee_edge\Plugin\Field\FieldType\ApigeeEdgeDeveloperIdFieldItem;
use Drupal\apigee_edge\Plugin\Validation\Constraint\DeveloperEmailUniqueValidator;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Template\Attribute;
use Drupal\Core\TempStore\TempStoreException;
use Drupal\Core\Url;
use Drupal\Core\Utility\Error;
use Drupal\user\UserInterface;
use Drupal\Component\Utility\Html;
define('APIGEE_EDGE_USER_REGISTRATION_SOURCE', 'apigee_edge_user_registration_source');

/**
 * Implements hook_module_implements_alter().
 */
function apigee_edge_module_implements_alter(&$implementations, $hook) {
  if (in_array($hook, [
    'form_user_register_form_alter',
    'form_user_form_alter',
  ])) {

    // Move apigee_edge_form_user_register_form_alter() and
    // apigee_edge_form_user_form_alter() alter hook implementations to the
    // end of the list.
    $group = $implementations['apigee_edge'];
    unset($implementations['apigee_edge']);
    $implementations['apigee_edge'] = $group;
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function apigee_edge_form_apigee_edge_authentication_form_alter(array &$form, FormStateInterface $form_state, string $form_id) {

  /** @var bool $do_not_alter_key_entity_forms */
  $do_not_alter_key_entity_forms = \Drupal::config('apigee_edge.dangerzone')
    ->get('do_not_alter_key_entity_forms');

  // Even if the original Key forms should not be altered, the Authentication
  // form provided by this module should still work the same.
  if ($do_not_alter_key_entity_forms) {

    /** @var \Drupal\apigee_edge\KeyEntityFormEnhancer $key_entity_form_enhancer */
    $key_entity_form_enhancer = \Drupal::service('apigee_edge.key_entity_form_enhancer');
    $key_entity_form_enhancer
      ->alterForm($form, $form_state);
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function apigee_edge_form_key_form_alter(array &$form, FormStateInterface $form_state, string $form_id) {

  /** @var bool $do_not_alter_key_entity_forms */
  $do_not_alter_key_entity_forms = \Drupal::config('apigee_edge.dangerzone')
    ->get('do_not_alter_key_entity_forms');

  // Even if the original Key forms should not be altered, the Authentication
  // form provided by this module should still work the same.
  if ($do_not_alter_key_entity_forms) {
    return;
  }

  /** @var \Drupal\apigee_edge\KeyEntityFormEnhancer $key_entity_form_enhancer */
  $key_entity_form_enhancer = \Drupal::service('apigee_edge.key_entity_form_enhancer');

  // Only those Key forms gets altered that defines an Apigee Edge key type.
  $key_entity_form_enhancer
    ->alterForm($form, $form_state);
}

/**
 * Implements hook_theme().
 */
function apigee_edge_theme() {
  return [
    'apigee_entity' => [
      'render element' => 'elements',
    ],
    'apigee_entity_list' => [
      'render element' => 'elements',
    ],
    'apigee_entity__app' => [
      'render element' => 'elements',
      'base hook' => 'apigee_entity',
    ],
    'app_credential' => [
      'render element' => 'elements',
    ],
    'app_credential_product_list' => [
      'render element' => 'elements',
    ],
    'status_property' => [
      'render element' => 'element',
    ],
    'apigee_secret' => [
      'render element' => 'elements',
      'base hook' => 'apigee_secret',
    ],
  ];
}

/**
 * Preprocess variables for the apigee_secret element template.
 */
function template_preprocess_apigee_secret(&$variables) {
  $variables['value'] = [
    '#markup' => $variables['elements']['#value'],
  ];
}

/**
 * Prepares variables for Apigee entity templates.
 *
 * Default template: apigee-entity.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - elements: An array of elements to display in view mode.
 */
function template_preprocess_apigee_entity(array &$variables) {
  $variables['view_mode'] = $variables['elements']['#view_mode'];

  /** @var \Drupal\apigee_edge\Entity\EdgeEntityInterface $entity */
  $entity = $variables['entity'] = $variables['elements']['#entity'];
  $variables['label'] = $entity
    ->label();
  if (!$entity
    ->isNew() && $entity
    ->hasLinkTemplate('canonical')) {
    $variables['url'] = $entity
      ->toUrl('canonical', [
      'language' => $entity
        ->language(),
    ])
      ->toString();
  }

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

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

  /** @var \Drupal\apigee_edge\Entity\EdgeEntityInterface $entity */
  $entity = $variables['elements']['#entity'];
  $sanitized_view_mode = str_replace('.', '_', $variables['elements']['#view_mode']);
  if ($entity instanceof AppInterface) {
    $suggestions[] = 'apigee_entity__app';
    $suggestions[] = 'apigee_entity__app__' . $sanitized_view_mode;
  }
  $suggestions[] = 'apigee_entity__' . $entity
    ->getEntityTypeId();
  $suggestions[] = 'apigee_entity__' . $entity
    ->getEntityTypeId() . '__' . $sanitized_view_mode;
  return $suggestions;
}

/**
 * Prepares variables for Apigee entity list templates.
 *
 * Default template: apigee-entity-list.html.twig.
 */
function template_preprocess_apigee_entity_list(array &$variables) {
  $variables['view_mode'] = $variables['elements']['#view_mode'];
  $variables['entity_type_id'] = $variables['elements']['#entity_type']
    ->id();
  $variables += [
    'content' => [],
  ];
  foreach (Element::children($variables['elements']) as $key) {
    $variables['content'][$key] = $variables['elements'][$key];
  }
}

/**
 * Implements hook_theme_suggestions_HOOK().
 */
function apigee_edge_theme_suggestions_apigee_entity_list(array $variables) {
  $suggestions = [];
  $view_mode = $variables['elements']['#view_mode'];
  $entity_type_id = $variables['elements']['#entity_type']
    ->id();

  // Add a suggestion based on the entity type and on the view mode.
  $suggestions[] = 'apigee_entity_list__' . $entity_type_id;
  $suggestions[] = 'apigee_entity_list__' . $entity_type_id . '__' . $view_mode;
  return $suggestions;
}

/**
 * Prepares variables for status_property templates.
 *
 * Default template: status_property.html.twig.
 *
 * @param array $variables
 *   An associative array.
 */
function template_preprocess_status_property(array &$variables) {
  $element =& $variables['element'];
  $element['value'] = $element['#value'];
  $classes = [
    'status-value-' . Html::getClass($element['#value']),
  ];
  if ($element['#indicator_status'] !== '') {
    $classes[] = str_replace('_', '-', $element['#indicator_status']);
  }
  $element['attributes'] = new Attribute([
    'class' => $classes,
  ]);
}

/**
 * Implements hook_system_breadcrumb_alter().
 */
function apigee_edge_system_breadcrumb_alter(Breadcrumb &$breadcrumb, RouteMatchInterface $route_match, array $context) {

  // Remove breadcrumb cache from every path under "/user" to let
  // CreateAppForDeveloperBreadcrumbBuilder build breadcrumb properly on the
  // add developer app for developer page.
  if (preg_match('/^\\/user.*/', $route_match
    ->getRouteObject()
    ->getPath())) {
    $breadcrumb
      ->mergeCacheMaxAge(0);
  }
  if ($route_match
    ->getRouteName() === 'entity.developer_app.add_form_for_developer') {
    $collection_route_by_developer_name = 'entity.developer_app.collection_by_developer';

    /** @var \Drupal\Core\Controller\TitleResolverInterface $title_resolver */
    $title_resolver = \Drupal::service('title_resolver');

    /** @var \Drupal\Core\Routing\RouteProviderInterface $route_provider */
    $route_provider = \Drupal::service('router.route_provider');
    $breadcrumb
      ->addLink(Link::createFromRoute($title_resolver
      ->getTitle(\Drupal::requestStack()
      ->getCurrentRequest(), $route_provider
      ->getRouteByName($collection_route_by_developer_name)), $collection_route_by_developer_name, [
      'user' => $route_match
        ->getParameter('user')
        ->id(),
    ]));
  }
}

/**
 * Implements hook_entity_base_field_info().
 */
function apigee_edge_entity_base_field_info(EntityTypeInterface $entity_type) {
  $fields = [];
  if ($entity_type
    ->id() === 'user') {
    $fields['first_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('First name'))
      ->setDescription(t('Your first name.'))
      ->setSetting('max_length', 64)
      ->setRequired(TRUE)
      ->setInitialValue('Firstname')
      ->setDisplayOptions('form', [
      'type' => 'string_textfield',
      'weight' => '-11',
      'settings' => [
        'display_label' => TRUE,
      ],
    ])
      ->setDisplayConfigurable('form', TRUE);
    $fields['last_name'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Last name'))
      ->setDescription(t('Your last name.'))
      ->setSetting('max_length', 64)
      ->setRequired(TRUE)
      ->setInitialValue('Lastname')
      ->setDisplayOptions('form', [
      'type' => 'string_textfield',
      'weight' => '-11',
      'settings' => [
        'display_label' => TRUE,
      ],
    ])
      ->setDisplayConfigurable('form', TRUE);
    $fields['apigee_edge_developer_id'] = BaseFieldDefinition::create('string')
      ->setName('apigee_edge_developer_id')
      ->setLabel(t('Apigee Edge Developer ID'))
      ->setComputed(TRUE)
      ->setClass(ApigeeEdgeDeveloperIdFieldItem::class);
  }
  return $fields;
}

/**
 * Implements hook_entity_base_field_info_alter().
 */
function apigee_edge_entity_base_field_info_alter(&$fields, EntityTypeInterface $entity_type) {

  /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields */
  if ($entity_type
    ->id() === 'user') {

    /** @var \Drupal\Core\Field\BaseFieldDefinition $mail */
    $mail = $fields['mail'];
    $mail
      ->setRequired(TRUE);
    $mail
      ->addConstraint('DeveloperMailUnique');

    // Add a bundle to these fields to allow other modules to display them
    // as configurable (fields added through the UI or configuration do have a
    // target bundle set).
    // @see https://github.com/apigee/apigee-edge-drupal/issues/396
    $fields['first_name']
      ->setTargetBundle('user');
    $fields['last_name']
      ->setTargetBundle('user');
  }
}

/**
 * Implements hook_entity_extra_field_info().
 */
function apigee_edge_entity_extra_field_info() {
  $extra = [];
  foreach (\Drupal::entityTypeManager()
    ->getDefinitions() as $definition) {
    if (in_array(AppInterface::class, class_implements($definition
      ->getOriginalClass()))) {

      // Bundles are not supported therefore both keys are the same.
      $extra[$definition
        ->id()][$definition
        ->id()]['display']['credentials'] = [
        'label' => new TranslatableMarkup('Credentials'),
        'description' => new TranslatableMarkup('Displays credentials provided by a @label', [
          '@label' => $definition
            ->getSingularLabel(),
        ]),
        // By default it should be displayed in the end of the view.
        'weight' => 100,
        'visible' => TRUE,
      ];
      $extra[$definition
        ->id()][$definition
        ->id()]['display']['warnings'] = [
        'label' => new TranslatableMarkup('Warnings'),
        'description' => new TranslatableMarkup('Displays app warnings'),
        'weight' => -100,
        'visible' => TRUE,
      ];
    }
  }
  return $extra;
}

/**
 * Implements hook_field_formatter_info_alter().
 */
function apigee_edge_field_formatter_info_alter(array &$info) {

  // Allows using the 'uri_link' formatter for the 'app_callback_url'
  // field type, which uses it as default formatter.
  // @see \Drupal\apigee_edge\Plugin\Field\FieldType\AppCallbackUrlItem
  $info['uri_link']['field_types'][] = 'app_callback_url';
}

/**
 * Implements hook_entity_view().
 */
function apigee_edge_entity_view(array &$build, EntityInterface $entity, EntityViewDisplayInterface $display, $view_mode) {
  if ($entity instanceof AppInterface) {

    // Add some required assets to an app's full entity view mode.
    $build['#attached']['library'][] = 'apigee_edge/apigee_edge.components';
    $build['#attached']['library'][] = 'apigee_edge/apigee_edge.app_view';
    if (\Drupal::moduleHandler()
      ->moduleExists('apigee_edge_teams')) {
      if ($team = \Drupal::routeMatch()
        ->getParameter('team')) {
        $team_app_name = $team
          ->getName();
      }
    }
    if ($user = \Drupal::routeMatch()
      ->getParameter('user')) {
      $build['#attached']['drupalSettings']['currentUser'] = $user
        ->id();
    }
    if ($display
      ->getComponent('credentials')) {

      /** @var \Drupal\apigee_edge\Entity\AppInterface $entity */
      $defaults = [
        '#cache' => [
          'contexts' => $entity
            ->getCacheContexts(),
          'tags' => $entity
            ->getCacheTags(),
        ],
      ];
      $build['credentials'] = [
        '#type' => 'container',
      ];
      $index = 0;
      foreach ($entity
        ->getCredentials() as $credential) {
        $build['credentials'][$credential
          ->getStatus()][] = [
          '#type' => 'app_credential',
          '#credential' => $credential,
          '#app_name' => $entity
            ->getName(),
          '#team_app_name' => isset($team_app_name) ? $team_app_name : '',
          '#app' => $entity,
          '#attributes' => [
            'class' => 'items--inline',
            'data-app' => $entity
              ->getName(),
            'data-team' => isset($team_app_name) ? $team_app_name : '',
            'data-app-container-index' => $index,
          ],
        ] + $defaults;
        $index++;
      }

      // Hide revoked credentials in a collapsible section.
      if (!empty($build['credentials'][AppCredentialInterface::STATUS_REVOKED])) {
        $revoked_credentials = $build['credentials'][AppCredentialInterface::STATUS_REVOKED];
        $build['credentials'][AppCredentialInterface::STATUS_REVOKED] = [
          '#type' => 'details',
          '#title' => t('Revoked keys (@count)', [
            '@count' => count($revoked_credentials),
          ]),
          '#weight' => 100,
          'credentials' => $revoked_credentials,
        ];
      }
    }

    // Add link to add keys.
    if ($entity
      ->access('add_api_key') && $entity
      ->hasLinkTemplate('add-api-key-form')) {
      $build['add_keys'] = Link::fromTextAndUrl(t('Add key'), $entity
        ->toUrl('add-api-key-form', [
        'attributes' => [
          'data-dialog-type' => 'modal',
          'data-dialog-options' => json_encode([
            'width' => 500,
            'height' => 250,
            'draggable' => FALSE,
            'autoResize' => FALSE,
          ]),
          'class' => [
            'use-ajax',
            'button',
          ],
        ],
      ]))
        ->toRenderable();
    }
  }
  if ($display
    ->getComponent('warnings')) {

    /** @var \Drupal\apigee_edge\Entity\AppWarningsCheckerInterface $app_warnings_checker */
    $app_warnings_checker = \Drupal::service('apigee_edge.entity.app_warnings_checker');
    $warnings = array_filter($app_warnings_checker
      ->getWarnings($entity));
    if (count($warnings)) {
      $build['warnings'] = [
        '#theme' => 'status_messages',
        '#message_list' => [
          'warning' => $warnings,
        ],
      ];
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_access().
 *
 * Supported operations: view, view label, assign.
 *
 * "assign" is a custom entity on API Products. It is being used on app
 * create/edit forms. A developer may have view access to an API product but
 * they can not assign it to an app (they can not obtain an API key for that
 * API product).
 *
 * Rules:
 * - The user gets allowed if has "Bypass API Product access control"
 * permission always.
 * - If operation is "view" or "view label" then the user gets access allowed
 * for the API Product (entity) if the API product's access attribute value is
 * either one of the selected access attribute values OR if a developer
 * app is in association with the selected API product.
 * - If operation is "assign" then disallow access if the role is configured
 * in the "Access by visibility" settings at the route
 * apigee_edge.settings.developer.api_product_access.
 */
function apigee_edge_api_product_access(EntityInterface $entity, $operation, AccountInterface $account) {

  /** @var \Drupal\apigee_edge\Entity\ApiProductInterface $entity */
  if (!in_array($operation, [
    'view',
    'view label',
    'assign',
  ])) {
    return AccessResult::neutral(sprintf('%s is not supported by %s.', $operation, __FUNCTION__));
  }
  $config_name = 'apigee_edge.api_product_settings';
  $result = AccessResult::allowedIfHasPermission($account, 'bypass api product access control');
  if ($result
    ->isNeutral()) {

    // Attribute may not exists but in that case it means public.
    $product_visibility = $entity
      ->getAttributeValue('access') ?? 'public';
    $visible_to_roles = \Drupal::config($config_name)
      ->get('access')[$product_visibility] ?? [];

    // A user may not have access to this API product based on the current
    // access setting but we should still grant view access
    // if they have a developer app in association with this API product.
    if (empty(array_intersect($visible_to_roles, $account
      ->getRoles()))) {
      if ($operation === 'assign') {

        // If the apigee_edge.settings.developer.api_product_access settings
        // limits access to this API product, do not allow user to assign it
        // to an application.
        $result = AccessResult::forbidden("User {$account->getEmail()} is does not have permissions to see API Product with visibility {$product_visibility}.");
      }
      else {
        $result = _apigee_edge_user_has_an_app_with_product($entity
          ->id(), $account, TRUE);
      }
    }
    else {
      $result = AccessResult::allowed();
    }
  }

  // If the API product gets updated it should not have any effect this
  // access control so we did not add $entity as a dependency to the result.
  return $result
    ->cachePerUser()
    ->addCacheTags([
    'config:' . $config_name,
  ]);
}

/**
 * Implements hook_entity_access().
 */
function apigee_edge_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
  if (!$entity
    ->getEntityType()
    ->entityClassImplements(AppInterface::class) || !in_array($operation, [
    'revoke_api_key',
    'delete_api_key',
  ])) {
    return AccessResult::neutral();
  }

  /** @var \Drupal\apigee_edge\Entity\AppInterface $entity **/
  $approved_credentials = array_filter($entity
    ->getCredentials(), function (AppCredentialInterface $credential) {
    return $credential
      ->getStatus() === AppCredentialInterface::STATUS_APPROVED;
  });

  // Prevent revoking/deleting the only active key.
  if (count($approved_credentials) <= 1) {
    $action = $operation === "revoke_api_key" ? "revoke" : "delete";
    return AccessResult::forbidden("You cannot {$action} the only active key.");
  }
}

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

/**
 * Extra validation for the entity_form_display.edit form of developer 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_developer_app_entity_form_display_edit_form_validate(array &$form, FormStateInterface $form_state) {
  $required = \Drupal::config('apigee_edge.developer_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'],
      ]));
    }
  }
}

/**
 * After build callback for verification email content form element.
 *
 * @param array $form_element
 *   Form element array.
 * @param Drupal\Core\Form\FormStateInterface $form_state
 *   Form state object.
 *
 * @return array
 *   Form array.
 *
 * @see \Drupal\apigee_edge\Form\DeveloperSettingsForm::buildForm
 */
function apigee_edge_developer_settings_form_verification_email_body_after_build(array $form_element, FormStateInterface $form_state) {
  if (isset($form_element['format'])) {

    // Hide input format settings when textarea itself is also hidden.
    $form_element['format']['#states']['visible'] = $form_element['#states']['visible'];
  }
  return $form_element;
}

/**
 * Implements hook_mail().
 *
 * Based on user_mail().
 */
function apigee_edge_mail($key, &$message, $params) {
  $token_service = \Drupal::token();
  $language_manager = \Drupal::languageManager();
  $langcode = $message['langcode'];

  /** @var \Drupal\Core\Session\AccountInterface $account */
  $account = $params['account'];
  $variables = [
    'user' => $account,
  ];
  $language = $language_manager
    ->getLanguage($account
    ->getPreferredLangcode());
  $original_language = $language_manager
    ->getConfigOverrideLanguage();
  $language_manager
    ->setConfigOverrideLanguage($language);
  $config = \Drupal::config('apigee_edge.developer_settings');
  $token_options = [
    'langcode' => $langcode,
    'callback' => '_apigee_edge_existing_developer_mail_tokens',
    'clear' => TRUE,
  ];
  $message['subject'] .= PlainTextOutput::renderFromHtml($token_service
    ->replace($config
    ->get('verification_email.subject'), $variables, $token_options));
  $message['body'][] = $token_service
    ->replace($config
    ->get('verification_email.body'), $variables, $token_options);
  $language_manager
    ->setConfigOverrideLanguage($original_language);
}

/**
 * Token callback to add unsafe tokens for existing developer user mails.
 *
 * This function is used by \Drupal\Core\Utility\Token::replace() to set up
 * some additional tokens that can be used in email messages generated by
 * apigee_edge_mail().
 *
 * @param array $replacements
 *   An associative array variable containing mappings from token names to
 *   values (for use with strtr()).
 * @param array $data
 *   An associative array of token replacement values. If the 'user' element
 *   exists, it must contain a user account object with the following
 *   properties:
 *   - login: The UNIX timestamp of the user's last login.
 *   - pass: The hashed account login password.
 * @param array $options
 *   A keyed array of settings and flags to control the token replacement
 *   process. See \Drupal\Core\Utility\Token::replace().
 */
function _apigee_edge_existing_developer_mail_tokens(array &$replacements, array $data, array $options) {
  if (isset($data['user'])) {
    $replacements['[user:developer-email-verification-url]'] = _apigee_edge_existing_developer_email_verification_link($data['user'], $options);
  }
}

/**
 * Sends a verification email to the developer email that is already taken.
 *
 * @param \Drupal\Core\Session\AccountInterface $account
 *   The user object of the account being notified. Must contain at
 *   least the fields 'uid', 'name', and 'mail'.
 * @param string $langcode
 *   (optional) Language code to use for the notification, overriding account
 *   language.
 *
 * @return array
 *   An array containing various information about the message.
 *   See \Drupal\Core\Mail\MailManagerInterface::mail() for details.
 *
 * @see \_apigee_edge_existing_developer_mail_tokens()
 */
function _apigee_edge_send_developer_email_verification_email(AccountInterface $account, $langcode = NULL) {
  if (\Drupal::config('apigee_edge.developer_settings')
    ->get('verification_action') === DeveloperSettingsForm::VERIFICATION_ACTION_VERIFY_EMAIL) {
    $params['account'] = $account;
    $langcode = $langcode ? $langcode : $account
      ->getPreferredLangcode();

    // Get the custom site notification email to use as the from email address
    // if it has been set.
    $site_mail = \Drupal::config('system.site')
      ->get('mail_notification');

    // If the custom site notification email has not been set, we use the site
    // default for this.
    if (empty($site_mail)) {
      $site_mail = \Drupal::config('system.site')
        ->get('mail');
    }
    if (empty($site_mail)) {
      $site_mail = ini_get('sendmail_from');
    }
    $mail = \Drupal::service('plugin.manager.mail')
      ->mail('apigee_edge', 'developer_email_verification', $account
      ->getEmail(), $langcode, $params, $site_mail);

    // TODO Should we notify admins about this?
  }
  return empty($mail) ? NULL : $mail['result'];
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function apigee_edge_form_user_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  // Only alter the user edit form.
  if ($form_id === 'user_register_form') {
    return;
  }

  // The email field should be always required because it is required on
  // Apigee Edge.
  $form['account']['mail']['#required'] = TRUE;
  $user = \Drupal::currentUser();

  // Make the same information available here as on user_register_form.
  // @see \Drupal\user\RegisterForm::form()
  $form['administer_users'] = [
    '#type' => 'value',
    '#value' => $user
      ->hasPermission('administer users'),
  ];

  // Add the API connection custom validation callback to the beginning of the
  // chain apigee_edge_module_implements_alter() ensures that form_alter hook is
  // called in the last time.
  $validation_functions[] = 'apigee_edge_form_user_form_api_connection_validate';

  // Add email custom validation callback to the chain immediately after the API
  // connection validation apigee_edge_module_implements_alter() ensures that
  // form_alter hook is called in the last time.
  $validation_functions[] = 'apigee_edge_form_user_form_developer_email_validate';
  $form['#validate'] = array_merge($validation_functions, $form['#validate']);
}

/**
 * Validates whether the provided email address is already taken on Apigee Edge.
 *
 * @param array $form
 *   Form array.
 * @param Drupal\Core\Form\FormStateInterface $form_state
 *   Form state object.
 */
function apigee_edge_form_user_form_developer_email_validate(array $form, FormStateInterface $form_state) {

  // If email address was changed.
  if ($form_state
    ->getValue('mail') !== $form_state
    ->getBuildInfo()['callback_object']
    ->getEntity()->mail->value) {
    $developer = NULL;
    try {
      $developer = Developer::load($form_state
        ->getValue('mail'));
    } catch (\Exception $exception) {

      // Nothing to do here, if there is no connection to Apigee Edge interrupt
      // the registration in the
      // apigee_edge_form_user_form_api_connection_validate() function.
    }
    if ($developer) {

      // Add email address to the whitelist because we would like to
      // display a custom error message instead of what this
      // field validation handler returns.
      DeveloperEmailUniqueValidator::whitelist($form_state
        ->getValue('mail'));
      if ($form_state
        ->getValue('administer_users')) {
        $form_state
          ->setErrorByName('mail', t('This email address already belongs to a developer on Apigee Edge.'));
      }
      else {
        $config = Drupal::config('apigee_edge.developer_settings');
        $form_state
          ->setErrorByName('mail', $config
          ->get('user_edit_error_message.value'));
      }
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function apigee_edge_form_user_register_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $validation_functions = [];

  // The email field should be always required because it is required on
  // Apigee Edge.
  $form['account']['mail']['#required'] = TRUE;

  // Add the API connection custom validation callback to the beginning of the
  // chain apigee_edge_module_implements_alter() ensures that form_alter hook is
  // called in the last time.
  $validation_functions[] = 'apigee_edge_form_user_form_api_connection_validate';
  $userInput = $form_state
    ->getUserInput();

  // Because this form alter is called earlier than the validation callback
  // (and the entity validations by user module) we have to use raw
  // user input here to check whether this form element should be visible
  // next time when the form is displayed on the UI with validation errors.
  if (!empty($userInput['mail']) && $form['account']['mail']['#default_value'] !== $userInput['mail']) {

    // Only add our extra features to the form if the provided email does not
    // belong to a user in Drupal yet. Otherwise let Drupal's build-in
    // validation to handle this problem.
    $user = user_load_by_mail($userInput['mail']);
    if (!$user) {

      // Add email custom validation callback to the chain immediately after the
      // API connection validation apigee_edge_module_implements_alter() ensures
      // that form_alter hook is called in the last time.
      $validation_functions[] = 'apigee_edge_form_user_register_form_developer_email_validate';
      try {
        $developer = Developer::load($userInput['mail']);
        $form['developer_exists'] = [
          '#type' => 'value',
          '#value' => (bool) $developer,
        ];
        if ($developer) {
          if ($form['administer_users']['#value']) {
            $form['account']['apigee_edge_developer_exists'] = [
              '#type' => 'checkbox',
              '#title' => t('I understand the provided email address belongs to a developer on Apigee Edge and I confirm user creation'),
              '#required' => TRUE,
              '#weight' => 0,
            ];
          }
          else {
            $config = Drupal::config('apigee_edge.developer_settings');
            if ($config
              ->get('verification_action') === DeveloperSettingsForm::VERIFICATION_ACTION_VERIFY_EMAIL) {
              $form['account']['apigee_edge_developer_unreceived_mail'] = [
                '#type' => 'checkbox',
                '#title' => t('I did not get an email. Please send me a new one.'),
                '#weight' => 0,
              ];
            }
          }
        }
      } catch (\Exception $exception) {

        // Nothing to do here, if there is no connection to Apigee Edge
        // Nothing to do here, if there is no connection to Apigee Edge
        // interrupt the registration in the
        // apigee_edge_form_user_form_api_connection_validate() function.
      }
    }
  }
  $form['#validate'] = array_merge($validation_functions, $form['#validate']);
  $form['#after_build'][] = 'apigee_edge_form_user_register_form_after_build';
}

/**
 * After build function for user_registration_form.
 *
 * @param array $form
 *   Form array.
 * @param Drupal\Core\Form\FormStateInterface $form_state
 *   Form state object.
 *
 * @return array
 *   Form array.
 */
function apigee_edge_form_user_register_form_after_build(array $form, FormStateInterface $form_state) {
  if (isset($form['account']['apigee_edge_developer_exists']) && isset($form['account']['mail'])) {
    $form['account']['apigee_edge_developer_exists']['#weight'] = $form['account']['mail']['#weight'] + 0.0001;
  }
  if (isset($form['account']['apigee_edge_developer_unreceived_mail']) && isset($form['account']['mail'])) {
    $form['account']['apigee_edge_developer_unreceived_mail']['#weight'] = $form['account']['mail']['#weight'] + 0.0001;
  }
  return $form;
}

/**
 * Validates whether there is connection to Apigee Edge or not.
 *
 * @param array $form
 *   Form array.
 * @param Drupal\Core\Form\FormStateInterface $form_state
 *   Form state object.
 */
function apigee_edge_form_user_form_api_connection_validate(array $form, FormStateInterface $form_state) {

  // If there is no connection to Apigee Edge interrupt the registration/user
  // update, otherwise it could be a security leak if a developer exists in
  // Apigee Edge with the same email address.

  /** @var \Drupal\apigee_edge\SDKConnectorInterface $sdk_connector */
  $sdk_connector = \Drupal::service('apigee_edge.sdk_connector');
  try {
    $sdk_connector
      ->testConnection();
  } catch (\Exception $exception) {
    $context = [
      '@user_email' => $form_state
        ->getValue('mail'),
      '@message' => (string) $exception,
    ];
    watchdog_exception('apigee_edge', $exception, 'Could not create/update Drupal user: @user_email, because there was no connection to Apigee Edge. @message %function (line %line of %file). <pre>@backtrace_string</pre>', $context);
    $form_state
      ->setError($form, t('User registration is temporarily unavailable. Try again later or contact the site administrator.'));
  }
}

/**
 * Validates whether the provided email address is already taken on Apigee Edge.
 *
 * @param array $form
 *   Form array.
 * @param Drupal\Core\Form\FormStateInterface $form_state
 *   Form state object.
 */
function apigee_edge_form_user_register_form_developer_email_validate(array $form, FormStateInterface $form_state) {

  // Do nothing if the developer does not exists.
  if (empty($form_state
    ->getValue('developer_exists'))) {
    return;
  }

  /** @var \Drupal\user\RegisterForm $registerForm */
  $registerForm = $form_state
    ->getFormObject();

  // Pass this information to hook_user_presave() in case if we would get
  // there.
  $registerForm
    ->getEntity()->{APIGEE_EDGE_USER_REGISTRATION_SOURCE} = 'user_register_form';

  // Do nothing if user has administer users permission.
  // (Form is probably displayed on admin/people/create.)
  if ($form_state
    ->getValue('administer_users')) {

    // Add email address to the whitelist because we do not want to
    // display the same error message for an admin user as a regular user.
    DeveloperEmailUniqueValidator::whitelist($form_state
      ->getValue('mail'));

    // If administrator has not confirmed that they would like to create a user
    // in Drupal with an existing developer id on Apigee Edge then add a custom
    // error to the field.
    if (empty($form_state
      ->getValue('apigee_edge_developer_exists'))) {
      $form_state
        ->setErrorByName('mail', t('This email address already exists on Apigee Edge.'));
    }
    return;
  }
  $config = \Drupal::config('apigee_edge.developer_settings');
  $request = \Drupal::request();
  $token = $request->query
    ->get($config
    ->get('verification_token'));
  $timestamp = $request->query
    ->get('timestamp');

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

  // Build user object from the submitted form values.
  $account = $registerForm
    ->buildEntity($form, $form_state);

  // If required parameters are available in the url.
  if ($token && $timestamp) {

    // If token is (still) valid then account's email to the whitelist of the
    // validator. This way it is not going to throw a validation error for this
    // email this time.
    if (apigee_edge_existing_developer_registration_hash_validate($account, $token, $timestamp)) {
      DeveloperEmailUniqueValidator::whitelist($account
        ->getEmail());
      return;
    }
    else {

      // Let user known that the token in the url has expired.
      // Drupal sends a new verification email.
      $form_state
        ->setErrorByName('mail', t('Registration token expired or invalid. We have sent you a new link.'));
    }
  }

  // Use shared storage to keep track of sent verification emails.
  // Form state's storage can not be used for this purpose because its values
  // are being cleared for every new requests. Private storage is too private
  // in case of anonymous user because every page request creates a new, empty
  // private temp storage.
  $storage = \Drupal::service('tempstore.shared');

  /** @var \Drupal\Core\TempStore\PrivateTempStore $sendNotifications */
  $sendNotifications = $storage
    ->get('apigee_edge_developer_email_verification_sent');

  // Do not send multiple email verifications to the same email address
  // every time when form validation fails with an error.
  if (!$sendNotifications
    ->get($account
    ->getEmail()) || $form_state
    ->getValue('apigee_edge_developer_unreceived_mail')) {

    // Send verification email to the user.
    $result = _apigee_edge_send_developer_email_verification_email($account, $account
      ->getPreferredLangcode());
    try {
      $sendNotifications
        ->set($account
        ->getEmail(), $result);
    } catch (TempStoreException $e) {
      watchdog_exception(__FUNCTION__, $e);
    }
  }
}

/**
 * Generates an URL to confirm identity of a user with existing developer mail.
 *
 * Based on user_cancel_url().
 *
 * @param \Drupal\user\UserInterface $account
 *   User object.
 * @param array $options
 *   (optional) A keyed array of settings. Supported options are:
 *   - langcode: A language code to be used when generating locale-sensitive
 *     URLs. If langcode is NULL the users preferred language is used.
 *
 * @return string
 *   A unique URL that may be used to confirm the cancellation of the user
 *   account.
 *
 * @see \_apigee_edge_existing_developer_mail_tokens()
 * @see \Drupal\user\Controller\UserController::confirmCancel()
 */
function _apigee_edge_existing_developer_email_verification_link(UserInterface $account, array $options = []) {
  $languageManager = \Drupal::languageManager();
  $timestamp = \Drupal::time()
    ->getRequestTime();
  $langcode = isset($options['langcode']) ? $options['langcode'] : $account
    ->getPreferredLangcode();
  $url_options = [
    'absolute' => TRUE,
    'language' => $languageManager
      ->getLanguage($langcode),
  ];
  $url_options['query'][\Drupal::config('apigee_edge.developer_settings')
    ->get('verification_token')] = apigee_edge_existing_developer_registration_hash($account, $timestamp);
  $url_options['query']['timestamp'] = $timestamp;

  // For now, use this method for generating url to the user register and
  // edit forms.
  $route = 'user.register';
  $route_params = [];
  if (!$account
    ->isAnonymous()) {
    $route = 'entity.user.edit_form';
    $route_params['user'] = $account
      ->id();
  }
  return Url::fromRoute($route, $route_params, $url_options)
    ->toString();
}

/**
 * Generates a token for an email address that is already taken on Apigee Edge.
 *
 * We do not want to enforce a user to use the same first name, last name,
 * username when this token is generated and when they re-open the registration
 * form by clicking on the link (that includes this token) from the verification
 * email. Therefore we only use the email address for token generation.
 *
 * Based on user_pass_rehash().
 *
 * @param \Drupal\user\UserInterface $account
 *   User object.
 * @param string $timestamp
 *   Timestamp for seed.
 *
 * @return string
 *   Generated token.
 */
function apigee_edge_existing_developer_registration_hash(UserInterface $account, string $timestamp) {
  $data = $account
    ->getEmail();
  $data .= $timestamp;

  // TODO Should we increase entropy by generating a random value for an email
  // address and temporary storing it with State API?
  return Crypt::hmacBase64($data, Settings::getHashSalt());
}

/**
 * Validates token for a registration with an existing developer email on Edge.
 *
 * @param \Drupal\user\UserInterface $account
 *   User object.
 * @param string $token
 *   Generated token from the url.
 * @param string $timestamp
 *   Timestamp from url.
 *
 * @return bool
 *   TRUE if token valid, false otherwise.
 */
function apigee_edge_existing_developer_registration_hash_validate(UserInterface $account, string $token, string $timestamp) {
  $current = \Drupal::time()
    ->getRequestTime();
  $timeout = \Drupal::config('apigee_edge.developer_settings')
    ->get('verification_token_expires');
  if ($timestamp <= $current && $current - $timestamp < $timeout && hash_equals($token, apigee_edge_existing_developer_registration_hash($account, $timestamp))) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Prepares variables for app_credential_product_list templates.
 *
 * Default template: app-credential-product-list.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - elements: An associative array containing the credential information.
 *     Properties used:
 *     - #credential_products: An \Apigee\Edge\Structure\CredentialProduct[]
 *       array. Array of products included in an app credential.
 *   - attributes: HTML attributes for the containing element.
 */
function template_preprocess_app_credential_product_list(array &$variables) {

  /** @var \Apigee\Edge\Structure\CredentialProduct[] $cred_products */
  $cred_products = $variables['elements']['#credential_products'];
  $cred_product_ids = array_map(function ($product) {

    /** @var \Apigee\Edge\Structure\CredentialProduct $product */
    return $product
      ->getApiproduct();
  }, $cred_products);

  /** @var \Drupal\apigee_edge\Entity\ApiProduct[] $allProducts */
  $variables['#api_product_entities'] = $allProducts = ApiProduct::loadMultiple($cred_product_ids);
  $variables += [
    'content' => [],
  ];
  foreach ($cred_products as $product) {
    if (!$allProducts[$product
      ->getApiproduct()]
      ->access('view label')) {
      continue;
    }
    $value = '';
    $indicator_status = '';
    switch ($product
      ->getStatus()) {
      case CredentialProduct::STATUS_APPROVED:
        $value = t('enabled');
        $indicator_status = StatusPropertyElement::INDICATOR_STATUS_OK;
        break;
      case CredentialProduct::STATUS_REVOKED:
        $value = t('disabled');
        $indicator_status = StatusPropertyElement::INDICATOR_STATUS_ERROR;
        break;
      case CredentialProduct::STATUS_PENDING:
        $value = t('pending');
        $indicator_status = StatusPropertyElement::INDICATOR_STATUS_WARNING;
        break;
    }
    $variables['content'][$product
      ->getApiproduct()] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => 'api-product-list-row clearfix',
      ],
      'label' => [
        '#type' => 'html_tag',
        '#tag' => 'span',
        '#value' => $allProducts[$product
          ->getApiproduct()]
          ->getDisplayName(),
        '#attributes' => [
          'class' => 'api-product-name',
        ],
      ],
      'status' => [
        '#type' => 'status_property',
        '#value' => $value,
        '#indicator_status' => $indicator_status,
      ],
    ];
  }
}

/**
 * Prepares variables for app_credential templates.
 *
 * Default template: app-credential.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - elements: An associative array containing the credential information.
 *     Properties used:
 *     - #credential: A \Apigee\Edge\Api\Management\Entity\AppCredential object.
 *       A developer app credential.
 *     - #app_name: string.
 *       App name.
 *     - #team_app_name: string.
 *       Team app name.
 *   - attributes: HTML attributes for the containing element.
 */
function template_preprocess_app_credential(array &$variables) {

  /** @var \Apigee\Edge\Api\Management\Entity\AppCredentialInterface $credential */
  $credential = $variables['elements']['#credential'];

  /** @var \Drupal\apigee_edge\Entity\AppInterface $app */
  $app = $variables['elements']['#app'];

  /** @var \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter */
  $dateFormatter = Drupal::service('date.formatter');
  $serializer = new AppCredentialSerializer();

  // Convert app entity to an array.
  $normalized = (array) $serializer
    ->normalize($credential);
  $properties_in_primary = [
    'consumerKey' => [
      'label' => t('Consumer Key'),
      'value_type' => 'plain',
    ],
    'consumerSecret' => [
      'label' => t('Consumer Secret'),
      'value_type' => 'plain',
    ],
    'issuedAt' => [
      'label' => t('Issued'),
      'value_type' => 'date',
    ],
    'expiresAt' => [
      'label' => t('Expires'),
      'value_type' => 'date',
    ],
    'status' => [
      'label' => t('Key Status'),
      'value_type' => 'status',
    ],
  ];
  $secret_properties = [
    'consumerKey',
    'consumerSecret',
  ];
  $variables['primary_wrapper'] = [
    '#type' => 'container',
    '#attributes' => [
      'class' => 'wrapper--primary app-details-wrapper',
    ],
  ];
  $index = 0;
  foreach ($properties_in_primary as $property => $def) {
    $variables['primary_wrapper'][$property] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => 'item-property',
      ],
    ];
    $variables['primary_wrapper'][$property]['label'] = [
      '#type' => 'label',
      '#title' => $def['label'],
      '#title_display' => 'before',
    ];
    $value = array_key_exists($property, $normalized) ? $normalized[$property] : NULL;
    if ($def['value_type'] == 'date') {

      // TODO Should we make format configurable?

      /** @var \DateTimeInterface $value */
      if ($value !== -1) {
        $time_diff = \Drupal::time()
          ->getRequestTime() - intval($value / 1000);
        if ($time_diff > 0) {
          $value = t('@time ago', [
            '@time' => $dateFormatter
              ->formatTimeDiffSince(intval($value / 1000)),
          ]);
        }
        else {
          $value = t('@time hence', [
            '@time' => $dateFormatter
              ->formatTimeDiffUntil(intval($value / 1000)),
          ]);
        }
      }
      else {
        $value = t('Never');
      }
    }

    // Below, $value is expected to be a string in some places, and it might be
    // TranslatableMarkup or a string. If it is not a string, then in some
    // cases warnings will be generated. This way it is always a string,
    // removing ambiguity.
    $value = (string) $value;
    if (in_array($property, $secret_properties)) {

      // Render the consumerKey and the consumerSecret as secret fields.
      $variables['primary_wrapper'][$property]['value'] = [
        '#type' => 'apigee_secret',
      ];
    }
    elseif ($def['value_type'] === 'status') {

      // Check if expired.
      if ($normalized['expiresAt'] !== -1 && \Drupal::time()
        ->getRequestTime() - (int) ($normalized['expiresAt'] / 1000) > 0) {
        $value = t('Expired');
      }
      $variables['primary_wrapper'][$property]['value'] = [
        '#type' => 'status_property',
        '#value' => $value,
        '#indicator_status' => $value === AppCredentialInterface::STATUS_APPROVED ? StatusPropertyElement::INDICATOR_STATUS_OK : StatusPropertyElement::INDICATOR_STATUS_ERROR,
      ];
    }
    else {
      $variables['primary_wrapper'][$property]['value'] = [
        '#markup' => Xss::filter($value),
      ];
    }
    $index++;
  }
  $variables['secondary_wrapper'] = [
    '#type' => 'container',
    '#attributes' => [
      'class' => 'wrapper--secondary',
    ],
    'title' => [
      '#type' => 'label',
      '#title_display' => 'before',
      '#title' => \Drupal::entityTypeManager()
        ->getDefinition('api_product')
        ->getPluralLabel(),
    ],
    'list' => [
      '#type' => 'app_credential_product_list',
      '#credential_products' => $credential
        ->getApiProducts(),
    ],
  ];

  // Helpful $content variable for templates.
  $variables['content'] = $normalized;

  // Add operations.
  $variables['operations'] = [
    '#type' => 'operations',
  ];
  if ($credential
    ->getStatus() === AppCredentialInterface::STATUS_APPROVED && $app
    ->access('revoke_api_key') && $app
    ->hasLinkTemplate('revoke-api-key-form')) {
    $variables['operations']['#links']['revoke'] = [
      'title' => t('Revoke'),
      'url' => $app
        ->toUrl('revoke-api-key-form')
        ->setRouteParameter('consumer_key', $credential
        ->getConsumerKey()),
    ];
  }
  if ($app
    ->access('delete_api_key') && $app
    ->hasLinkTemplate('delete-api-key-form')) {
    $variables['operations']['#links']['delete'] = [
      'title' => t('Delete'),
      'url' => $app
        ->toUrl('delete-api-key-form')
        ->setRouteParameter('consumer_key', $credential
        ->getConsumerKey()),
    ];
  }
}

/**
 * Implements hook_user_presave().
 *
 * TODO Take (configurable?) actions if a user could not be saved in Drupal but
 * it has been in Apigee Edge.
 */
function apigee_edge_user_presave(UserInterface $account) {

  // If the developer-user synchronization is in progress, then saving
  // developers while saving Drupal user should be avoided.
  if (_apigee_edge_is_sync_in_progress()) {
    return;
  }

  /** @var \Drupal\apigee_edge\UserDeveloperConverterInterface $user_developer */
  $user_developer = \Drupal::service('apigee_edge.converter.user_developer');

  /** @var \Drupal\apigee_edge\FieldAttributeConverterInterface $field_to_attribute */
  $field_to_attribute = \Drupal::service('apigee_edge.converter.field_attribute');

  /** @var \Drupal\Core\Logger\LoggerChannelInterface $logger */
  $logger = \Drupal::service('logger.channel.apigee_edge');
  try {

    /** @var \Drupal\apigee_edge\Entity\Developer $developer */
    $result = $user_developer
      ->convertUser($account);

    // There were no changes.
    if ($result
      ->getSuccessfullyAppliedChanges() === 0) {
      return;
    }

    // Log problems occurred meanwhile the conversion process.
    foreach ($result
      ->getProblems() as $conversionProblem) {
      $context = [
        '%mail' => $account
          ->getEmail(),
        'link' => $account
          ->toLink()
          ->toString(),
      ];
      if ($conversionProblem instanceof UserDeveloperConversionUserFieldDoesNotExistException) {
        $message = "Skipping %mail developer's %attribute_name attribute update because %field_name field does not exist.";
        $context['%field_name'] = $conversionProblem
          ->getFieldName();
        $context['%attribute_name'] = $field_to_attribute
          ->getAttributeName($conversionProblem
          ->getFieldName());
        $logger
          ->warning($message, $context);
      }
      elseif ($conversionProblem instanceof UserDeveloperConversionNoStorageFormatterFoundException) {
        $message = "Skipping %mail developer's %attribute_name attribute update because there is no available storage formatter for %field_type field type.";
        $context['%field_type'] = $conversionProblem
          ->getFieldDefinition()
          ->getType();
        $context['%attribute_name'] = $field_to_attribute
          ->getAttributeName($conversionProblem
          ->getFieldDefinition()
          ->getName());
        $logger
          ->warning($message, $context);
      }
      else {
        $logger
          ->warning($conversionProblem
          ->getMessage());
      }
    }
    $developer = $result
      ->getDeveloper();
    $developer
      ->save();
  } catch (\Exception $exception) {
    $previous = $exception
      ->getPrevious();
    $context = [
      '@developer' => $account
        ->getEmail(),
      '@message' => (string) $exception,
      // UID 1 (created meanwhile the install process by config_installer) is
      // not a new account.
      // @see \Drupal\config_installer\Form\SiteConfigureForm::submitForm()
      // Also, id() returns a string not an integer.
      '@operation' => $account
        ->isNew() || $account
        ->id() == 1 ? 'create' : 'update',
    ];
    if ($previous instanceof ClientErrorException && $previous
      ->getEdgeErrorCode()) {
      if ($previous
        ->getEdgeErrorCode() === Developer::APIGEE_EDGE_ERROR_CODE_DEVELOPER_DOES_NOT_EXISTS) {
        \Drupal::service('logger.channel.apigee_edge')
          ->info('Could not update @developer developer entity because it does not exist on Apigee Edge. Automatically trying to create a new developer entity.', $context);
        try {

          // Forcibly mark developer entity as new to send POST request to Edge
          // instead of PUT. This should be a better way then clearing
          // "originalEmail" property's value on the entity.
          $developer
            ->enforceIsNew(TRUE);
          $developer
            ->save();
        } catch (\Exception $exception) {
          $context = [
            '@developer' => $account
              ->getEmail(),
            '@message' => (string) $exception,
          ];
          $context += Error::decodeException($exception);
          $logger
            ->error('Could not create developer entity: @developer. @message %function (line %line of %file). <pre>@backtrace_string</pre>', $context);
        }
      }
      elseif ($previous
        ->getEdgeErrorCode() === Developer::APIGEE_EDGE_ERROR_CODE_DEVELOPER_ALREADY_EXISTS) {
        $logger
          ->info($previous
          ->getMessage());
        $developer = Developer::load($account
          ->getEmail());
        if ($developer) {
          $developer_id = $developer
            ->getDeveloperId();

          // If a user could register on the portal with an email address
          // that already belongs to a developer on Apigee Edge then override
          // its stored developer data there with the new one.
          if (isset($account->{APIGEE_EDGE_USER_REGISTRATION_SOURCE}) && $account->{APIGEE_EDGE_USER_REGISTRATION_SOURCE} === 'user_register_form') {
            $developer = $user_developer
              ->convertUser($account);
            $developer
              ->setDeveloperId($developer_id);
            $developer
              ->enforceIsNew(FALSE);
            try {
              $developer
                ->save();
            } catch (ApiException $exception) {
              $logger
                ->error("Unable to update existing @developer developer's data after registered on the portal.", $context);
            }
          }
        }
        else {
          $logger
            ->error("Unable to save @developer developer's developer id on user.", $context);
        }
      }
      elseif ($previous
        ->getEdgeErrorCode() === Developer::APIGEE_HYBRID_ERROR_CODE_DEVELOPER_EMAIL_MISMATCH) {

        // Apigee X and Hybrid runtime v1.5.0 and v1.5.1 a call to change the developer's
        // email address will not work so need to prevent user email update on
        // Drupal as well.
        // @see https://github.com/apigee/apigee-client-php/issues/153
        // @see https://github.com/apigee/apigee-edge-drupal/issues/587
        throw new DeveloperUpdateFailedException($account
          ->getEmail(), "Developer @email profile cannot be updated. " . $previous
          ->getMessage());
      }
    }
    else {
      $context += Error::decodeException($exception);
      $logger
        ->error('Could not @operation developer entity: @developer. @message %function (line %line of %file). <pre>@backtrace_string</pre>', $context);
    }
  }
}

/**
 * Implements hook_user_cancel().
 */
function apigee_edge_user_cancel(array $edit, UserInterface $account, $method) {
  if ($method === 'user_cancel_block_unpublish' || $method === 'user_cancel_block') {

    /** @var \Drupal\apigee_edge\UserDeveloperConverterInterface $user_developer */
    $user_developer = \Drupal::service('apigee_edge.converter.user_developer');

    /** @var \Drupal\Core\Logger\LoggerChannelInterface $logger */
    $logger = \Drupal::service('logger.channel.apigee_edge');
    try {

      /** @var \Drupal\apigee_edge\Entity\Developer $developer */
      $developer = $user_developer
        ->convertUser($account)
        ->getDeveloper();
      $developer
        ->save();
    } catch (\Exception $exception) {
      $context = [
        '@developer' => $account
          ->getEmail(),
        '@message' => (string) $exception,
      ];
      $context += Error::decodeException($exception);
      $logger
        ->error('Could not block @developer developer. @message %function (line %line of %file). <pre>@backtrace_string</pre>', $context);
    }
  }
}

/**
 * Implements hook_user_cancel_methods_alter().
 */
function apigee_edge_user_cancel_methods_alter(&$methods) {

  // Transform available keys to replacements that could be passed to t().
  $get_replacements = function (array $method_info) : array {
    $replacements = [];
    array_walk($method_info, function ($value, $key) use (&$replacements) {
      $replacements["@{$key}"] = $value;
    });
    return $replacements;
  };

  // Returns extra information for a method that we would like to display.
  // "title" is what a user with "administer users" permission can see,
  // "description" is for regular authenticated users by default.
  $get_extra_info = function (string $key, $replacements) : ?array {
    $extra_infos = [
      'user_cancel_block' => [
        'title' => t("@title Account's API credentials will be invalid until this account gets re-activated.", $replacements),
        'description' => t('@description <strong>Your API credentials will be invalid until your account is unblocked.</strong>', $replacements),
      ],
      'user_cancel_delete' => [
        'title' => t("@title <strong>All API apps and API credentials owned by this account will be deleted from Apigee Edge.</strong>", $replacements),
        'description' => t('@description <strong>Your API apps and API credentials will be deleted permanently.</strong>', $replacements),
      ],
    ];

    // The same warning should be displayed in these cancellation methods.
    $extra_infos['user_cancel_block_unpublish'] = $extra_infos['user_cancel_block'];
    $extra_infos['user_cancel_reassign'] = $extra_infos['user_cancel_delete'];
    return $extra_infos[$key] ?? NULL;
  };
  foreach ($methods as $method => $info) {
    $extra_info = $get_extra_info($method, $get_replacements($info));
    if ($extra_info) {
      $methods[$method] = array_merge($methods[$method], $extra_info);
    }
  }
}

/**
 * Implements hook_user_delete().
 */
function apigee_edge_user_delete(UserInterface $account) {

  // Do not try to delete developer of the anonymous user because it does
  // not exist.
  if ($account
    ->isAnonymous()) {
    return;
  }
  try {

    /** @var \Drupal\apigee_edge\Entity\Developer $developer */
    $developer = Developer::load($account
      ->getEmail());

    // Sanity check, the developer may not exist in Apigee Edge.
    if ($developer) {
      $developer
        ->delete();
    }
  } catch (\Exception $exception) {
    $context = [
      '@developer' => $account
        ->getEmail(),
      '@message' => (string) $exception,
    ];
    watchdog_exception('apigee_edge', $exception, 'Could not delete @developer developer entity. @message %function (line %line of %file). <pre>@backtrace_string</pre>', $context);
  }
}

/**
 * Implements hook_field_config_delete().
 *
 * Removes field name from the module's configuration after deleting the field.
 */
function apigee_edge_field_config_delete(EntityInterface $entity) {

  /** @var \Drupal\field\FieldConfigInterface $entity */
  $user_fields_to_sync = \Drupal::configFactory()
    ->get('apigee_edge.sync')
    ->get('user_fields_to_sync');
  \Drupal::configFactory()
    ->getEditable('apigee_edge.sync')
    ->set('user_fields_to_sync', array_diff($user_fields_to_sync, [
    $entity
      ->getName(),
  ]))
    ->save();
}

/**
 * Implements hook_form_field_ui_field_storage_add_form_alter().
 */
function apigee_edge_form_field_ui_field_storage_add_form_alter(array &$form, FormStateInterface &$form_state) {
  if ($form_state
    ->get('entity_type_id') === 'user') {
    $form['apigee_edge_sync'] = [
      '#type' => 'checkbox',
      '#title' => t('Synchronize to Apigee Edge'),
    ];
    $form['#submit'][] = 'apigee_edge_field_ui_field_storage_add_form_submit';
  }
}

/**
 * Custom submit handler for field_ui_field_storage_add_form.
 *
 * @param array $form
 *   An associative array containing the structure of the form.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   The current state of the form.
 */
function apigee_edge_field_ui_field_storage_add_form_submit(array &$form, FormStateInterface &$form_state) {
  if ($form_state
    ->getValue('apigee_edge_sync')) {
    $user_fields_to_sync = \Drupal::configFactory()
      ->get('apigee_edge.sync')
      ->get('user_fields_to_sync');
    $user_fields_to_sync[] = $form_state
      ->getValue('field_name');
    \Drupal::configFactory()
      ->getEditable('apigee_edge.sync')
      ->set('user_fields_to_sync', $user_fields_to_sync)
      ->save();
  }
}

/**
 * Implements hook_key_delete().
 */
function apigee_edge_key_delete(EntityInterface $entity) {

  /** @var \Drupal\key\KeyInterface $entity */
  $active_key = \Drupal::configFactory()
    ->get('apigee_edge.auth')
    ->get('active_key');
  if ($active_key === $entity
    ->id()) {
    \Drupal::configFactory()
      ->getEditable('apigee_edge.auth')
      ->set('active_key', '')
      ->save();
  }
}

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

  /** @var \Drupal\apigee_edge\JobExecutor $executor */
  $executor = \Drupal::service('apigee_edge.job_executor');

  // Schedules 100 items from the job table.
  // The reason of this is to avoid race conditions.
  for ($i = 0; $i < 100; $i++) {
    if ($job = $executor
      ->select()) {
      $executor
        ->cast($job);
    }
    else {
      break;
    }
  }
}

/**
 * Returns the job executor instance.
 *
 * @return \Drupal\apigee_edge\JobExecutor
 *   The job executor instance.
 */
function apigee_edge_get_executor() : JobExecutor {
  return \Drupal::service('apigee_edge.job_executor');
}

/**
 * Implements hook_preprocess_table().
 */
function apigee_edge_preprocess_table(&$variables) {
  if (isset($variables['attributes']['id']) && $variables['attributes']['id'] === 'app-list') {
    $variables['no_striping'] = TRUE;
    $index = 0;
    foreach ($variables['rows'] as $row) {
      if ($row['attributes']
        ->hasClass('row--info')) {
        if ($index % 2 === 0) {
          $row['attributes']
            ->addClass('odd');
          $index++;
        }
        else {
          $row['attributes']
            ->addClass('even');
          $index++;
        }
      }
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_storage_load().
 *
 * Set Drupal owner ids on developers after they were loaded from Apigee
 * Edge.
 */
function apigee_edge_developer_storage_load(array $entities) {
  $developerId_mail_map = [];

  /** @var \Drupal\apigee_edge\Entity\Developer $entity */
  foreach ($entities as $entity) {
    $developerId_mail_map[$entity
      ->getDeveloperId()] = $entity
      ->getEmail();
  }
  $query = \Drupal::database()
    ->select('users_field_data', 'ufd');
  $query
    ->fields('ufd', [
    'mail',
    'uid',
  ])
    ->condition('mail', $developerId_mail_map, 'IN');
  $mail_uid_map = $query
    ->execute()
    ->fetchAllKeyed();
  foreach ($entities as $entity) {

    // If developer id is not in this map it means the developer does not exist
    // in Drupal yet (developer syncing between Apigee Edge and Drupal is
    // required) or the developer id has not been stored in related Drupal user
    // yet. This can be fixed with running developer sync too, because it could
    // happen that the user had been created in Drupal before Apigee Edge
    // connected was configured. Although, this could be a result of a previous
    // error but there should be a log about that.
    if (isset($mail_uid_map[$developerId_mail_map[$entity
      ->getDeveloperId()]])) {
      $entity
        ->setOwnerId($mail_uid_map[$developerId_mail_map[$entity
        ->getDeveloperId()]]);
    }
  }
}

/**
 * Implements hook_ENTITY_TYPE_storage_load().
 *
 * Set Drupal owner ids on developer apps after they were loaded from Apigee
 * Edge.
 */
function apigee_edge_developer_app_storage_load(array $entities) {
  $developer_ids = [];

  /** @var \Drupal\apigee_edge\Entity\DeveloperApp $entity */
  foreach ($entities as $entity) {
    $developer_ids[] = $entity
      ->getDeveloperId();
  }
  $developer_ids = array_unique($developer_ids);
  $dev_id_mail_map = [];

  /** @var \Drupal\apigee_edge\Entity\Storage\DeveloperStorageInterface $developer_storage */
  $developer_storage = \Drupal::entityTypeManager()
    ->getStorage('developer');
  foreach ($developer_storage
    ->loadByProperties([
    'developerId' => $developer_ids,
  ]) as $developer) {

    /** @var \Drupal\apigee_edge\Entity\Developer $developer */
    $dev_id_mail_map[$developer
      ->uuid()] = $developer
      ->getEmail();
  }

  // Sanity check, IN condition below should not be used with an empty array.
  if (empty($dev_id_mail_map)) {
    return;
  }

  // We load all users once with related found email addresses because
  // \Drupal\apigee_edge\Entity\DeveloperApp::setOwnerId() would load them
  // anyway but one by one. With this we warm up the user entity cache and
  // User::load() will return all users from cache.
  $userStorage = \Drupal::entityTypeManager()
    ->getStorage('user');
  $uids = $userStorage
    ->getQuery()
    ->condition('mail', $dev_id_mail_map, 'IN')
    ->execute();
  $users = $userStorage
    ->loadMultiple($uids);
  if ($dev_id_mail_map) {
    $mail_developerId_map = array_flip($dev_id_mail_map);
    foreach ($users as $user) {
      if (array_key_exists($user->mail->value, $mail_developerId_map)) {
        $mail_uid_map[$user->mail->value] = $user
          ->id();
      }
    }
  }
  else {
    $mail_uid_map = [];
  }
  foreach ($entities as $entity) {

    // If developer id is not in this map it means the developer does not exist
    // in Drupal yet (developer syncing between Apigee Edge and Drupal is
    // required) or the developer id has not been stored in related Drupal user
    // yet. This can be fixed by running developer sync. The reason is simple,
    // it could happen that the user had been created in Drupal before Apigee
    // Edge connected was configured. Although, this could be a result of a
    // previous error but there should be a log about that.
    if (isset($dev_id_mail_map[$entity
      ->getDeveloperId()]) && isset($mail_uid_map[$dev_id_mail_map[$entity
      ->getDeveloperId()]])) {
      $entity
        ->setOwnerId($mail_uid_map[$dev_id_mail_map[$entity
        ->getDeveloperId()]]);
    }
  }
}

/**
 * Checks whether a user has a developer app with an API product.
 *
 * We did not add static caching to this function because there could be
 * possible scenarios when a cache invalidation issue could occur and this
 * function would return a false-positive result.
 *
 * @param string $product_name
 *   API Product name.
 * @param \Drupal\Core\Session\AccountInterface|null $account
 *   (optional) The user session for which to check access, or NULL to check
 *   access for the current user. Defaults to NULL.
 * @param bool $return_as_object
 *   (optional) Defaults to FALSE.
 *
 * @return \Drupal\Core\Access\AccessResultInterface|bool
 *   The access result. Returns a boolean if $return_as_object is FALSE (this
 *   is the default) and otherwise an AccessResultInterface object.
 *   When a boolean is returned, the result of AccessInterface::isAllowed() is
 *   returned, i.e. TRUE means access is explicitly allowed, FALSE means
 *   access is either explicitly forbidden or "no opinion".
 *
 * @see \Drupal\Core\Entity\EntityAccessControlHandlerInterface::access()
 */
function _apigee_edge_user_has_an_app_with_product(string $product_name, AccountInterface $account = NULL, bool $return_as_object = FALSE) {
  if ($account === NULL) {
    $account = \Drupal::currentUser();
  }
  if ($account
    ->isAnonymous()) {
    $result = AccessResult::neutral('Anonymous user does not have a developer account on Apigee Edge.');
  }
  else {

    /** @var \Drupal\apigee_edge\Entity\DeveloperAppInterface|null $app_with_product */
    $app_with_product = NULL;

    /** @var \Drupal\apigee_edge\Entity\Storage\DeveloperAppStorageInterface $developer_app_storage */
    $developer_app_storage = \Drupal::entityTypeManager()
      ->getStorage('developer_app');
    foreach ($developer_app_storage
      ->loadByDeveloper($account
      ->getEmail()) as $app) {

      /** @var \Apigee\Edge\Api\Management\Entity\AppCredentialInterface $credential */
      foreach ($app
        ->getCredentials() as $credential) {
        $product_ids = array_map(function (CredentialProduct $product) {
          return $product
            ->getApiproduct();
        }, $credential
          ->getApiProducts());

        // We return after the first match to speed up the page load.
        if (in_array($product_name, $product_ids)) {
          $app_with_product = $app;
          break 2;
        }
      }
    }
    if ($app_with_product) {

      // Flush cache if this app gets updated. It could happen that
      // this product gets removed from the app therefore the access
      // must be re-evaluated.
      $result = AccessResult::allowed()
        ->cachePerUser()
        ->addCacheTags($app_with_product
        ->getCacheTags());
    }
    else {
      $result = AccessResult::neutral("{$account->getDisplayName()} does not have any developer app in association with {$product_name} API product.");
    }
  }
  return $return_as_object ? $result : $result
    ->isAllowed();
}

/**
 * Indicates that the developer-user synchronization is in progress.
 *
 * If the developer-user synchronization is in progress, then saving
 * the same developer in apigee_edge_user_presave() while creating Drupal user
 * based on a developer should be avoided.
 *
 * @param bool|null $in_progress
 *   Developer-user synchronization state.
 *
 * @return bool
 *   TRUE if the developer-user synchronization is in progress, else FALSE.
 */
function _apigee_edge_set_sync_in_progress(?bool $in_progress = NULL) : bool {
  static $state;
  if ($in_progress !== NULL) {
    $state = $in_progress;
  }
  return $state ?? FALSE;
}

/**
 * Gets the developer synchronization state.
 *
 * @return bool
 *   TRUE if the developer-user synchronization is in progress, else FALSE.
 */
function _apigee_edge_is_sync_in_progress() : bool {
  return _apigee_edge_set_sync_in_progress();
}

/**
 * Implements hook_cache_flush().
 */
function apigee_edge_cache_flush() {

  // If OAuth token file does not exist this does not do anything.
  \Drupal::service('apigee_edge.authentication.oauth_token_storage')
    ->removeToken();
}

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

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

/**
 * Implements hook_preprocess_HOOK().
 */
function apigee_edge_preprocess_fieldset(&$variables) {

  // @todo This is a temporary fix for core issue #3174459.
  if ($variables['required'] == true) {
    if ($variables['attributes']['required']) {
      unset($variables['attributes']['required']);
    }
    if ($variables['attributes']['aria-required']) {
      unset($variables['attributes']['aria-required']);
    }
  }
}

Functions

Namesort descending Description
apigee_edge_api_product_access Implements hook_ENTITY_TYPE_access().
apigee_edge_app_listing_page_title Gets the title of app listing page.
apigee_edge_cache_flush Implements hook_cache_flush().
apigee_edge_cron Implements hook_cron().
apigee_edge_developer_app_storage_load Implements hook_ENTITY_TYPE_storage_load().
apigee_edge_developer_settings_form_verification_email_body_after_build After build callback for verification email content form element.
apigee_edge_developer_storage_load Implements hook_ENTITY_TYPE_storage_load().
apigee_edge_entity_access Implements hook_entity_access().
apigee_edge_entity_base_field_info Implements hook_entity_base_field_info().
apigee_edge_entity_base_field_info_alter Implements hook_entity_base_field_info_alter().
apigee_edge_entity_extra_field_info Implements hook_entity_extra_field_info().
apigee_edge_entity_view Implements hook_entity_view().
apigee_edge_existing_developer_registration_hash Generates a token for an email address that is already taken on Apigee Edge.
apigee_edge_existing_developer_registration_hash_validate Validates token for a registration with an existing developer email on Edge.
apigee_edge_field_config_delete Implements hook_field_config_delete().
apigee_edge_field_formatter_info_alter Implements hook_field_formatter_info_alter().
apigee_edge_field_ui_field_storage_add_form_submit Custom submit handler for field_ui_field_storage_add_form.
apigee_edge_form_apigee_edge_authentication_form_alter Implements hook_form_FORM_ID_alter().
apigee_edge_form_entity_form_display_edit_form_alter Implements hook_form_FORM_ID_alter().
apigee_edge_form_field_ui_field_storage_add_form_alter Implements hook_form_field_ui_field_storage_add_form_alter().
apigee_edge_form_key_form_alter Implements hook_form_BASE_FORM_ID_alter().
apigee_edge_form_user_form_alter Implements hook_form_FORM_ID_alter().
apigee_edge_form_user_form_api_connection_validate Validates whether there is connection to Apigee Edge or not.
apigee_edge_form_user_form_developer_email_validate Validates whether the provided email address is already taken on Apigee Edge.
apigee_edge_form_user_register_form_after_build After build function for user_registration_form.
apigee_edge_form_user_register_form_alter Implements hook_form_FORM_ID_alter().
apigee_edge_form_user_register_form_developer_email_validate Validates whether the provided email address is already taken on Apigee Edge.
apigee_edge_get_executor Returns the job executor instance.
apigee_edge_key_delete Implements hook_key_delete().
apigee_edge_mail Implements hook_mail().
apigee_edge_module_implements_alter Implements hook_module_implements_alter().
apigee_edge_preprocess_fieldset Implements hook_preprocess_HOOK().
apigee_edge_preprocess_table Implements hook_preprocess_table().
apigee_edge_system_breadcrumb_alter Implements hook_system_breadcrumb_alter().
apigee_edge_theme Implements hook_theme().
apigee_edge_theme_suggestions_apigee_entity Implements hook_theme_suggestions_HOOK().
apigee_edge_theme_suggestions_apigee_entity_list Implements hook_theme_suggestions_HOOK().
apigee_edge_user_cancel Implements hook_user_cancel().
apigee_edge_user_cancel_methods_alter Implements hook_user_cancel_methods_alter().
apigee_edge_user_delete Implements hook_user_delete().
apigee_edge_user_presave Implements hook_user_presave().
template_preprocess_apigee_entity Prepares variables for Apigee entity templates.
template_preprocess_apigee_entity_list Prepares variables for Apigee entity list templates.
template_preprocess_apigee_secret Preprocess variables for the apigee_secret element template.
template_preprocess_app_credential Prepares variables for app_credential templates.
template_preprocess_app_credential_product_list Prepares variables for app_credential_product_list templates.
template_preprocess_status_property Prepares variables for status_property templates.
_apigee_edge_developer_app_entity_form_display_edit_form_validate Extra validation for the entity_form_display.edit form of developer apps.
_apigee_edge_existing_developer_email_verification_link Generates an URL to confirm identity of a user with existing developer mail.
_apigee_edge_existing_developer_mail_tokens Token callback to add unsafe tokens for existing developer user mails.
_apigee_edge_is_sync_in_progress Gets the developer synchronization state.
_apigee_edge_send_developer_email_verification_email Sends a verification email to the developer email that is already taken.
_apigee_edge_set_sync_in_progress Indicates that the developer-user synchronization is in progress.
_apigee_edge_user_has_an_app_with_product Checks whether a user has a developer app with an API product.

Constants