SU-SWS/stanford_profile_helper

View on GitHub
modules/stanford_person/modules/stanford_person_importer/src/Cap.php

Summary

Maintainability
A
1 hr
Test Coverage
A
97%
<?php

namespace Drupal\stanford_person_importer;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\taxonomy\TermInterface;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;

/**
 * Stanford CAP API helper service.
 *
 * @package Drupal\stanford_person_importer
 */
class Cap implements CapInterface {

  use StringTranslationTrait;

  /**
   * CAPx API username.
   *
   * @var string
   */
  protected $clientId;

  /**
   * CAPx API password.
   *
   * @var string
   */
  protected $clientSecret;

  /**
   * Guzzle client service.
   *
   * @var \GuzzleHttp\ClientInterface
   */
  protected $client;

  /**
   * Entity type manager service.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Cache service.
   *
   * @var \Drupal\Core\Cache\CacheBackendInterface
   */
  protected $cache;

  /**
   * Database connection service.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $database;

  /**
   * Database logging service.
   *
   * @var \Drupal\Core\Logger\LoggerChannelInterface
   */
  protected $logger;

  /**
   * Validate the form submission credentials are valid.
   *
   * @param array $element
   *   Password form element.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Current form state.
   * @param array $form
   *   Complete form render array.
   */
  public static function validateCredentials(array $element, FormStateInterface $form_state, array $form) {
    $username = $form_state->getValue(['su_person_cap_username', 0, 'value']);
    $password = $form_state->getValue(['su_person_cap_password', 0, 'value']);

    // Call the service to test the connection.
    $success = \Drupal::service('stanford_person_importer.cap')
      ->setClientId($username)
      ->setClientSecret($password)
      ->testConnection();
    if (!$success) {
      $form_state->setError($element, 'Invalid CAP credentials.');
    }
  }

  /**
   * Capx constructor.
   *
   * @param \GuzzleHttp\ClientInterface $guzzle
   *   Guzzle http service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   Entity Type Manager Service.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   Cache service.
   * @param \Drupal\Core\Database\Connection $database
   *   Database connection service.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   Database logging service.
   */
  public function __construct(ClientInterface $guzzle, EntityTypeManagerInterface $entity_type_manager, CacheBackendInterface $cache, Connection $database, LoggerChannelFactoryInterface $logger_factory) {
    $this->client = $guzzle;
    $this->entityTypeManager = $entity_type_manager;
    $this->cache = $cache;
    $this->database = $database;
    $this->logger = $logger_factory->get('stanford_person_importer');
  }

  /**
   * {@inheritDoc}
   */
  public function setClientId(string $client_id): self {
    $this->clientId = $client_id;
    return $this;
  }

  /**
   * {@inheritDoc}
   */
  public function setClientSecret(string $secret): self {
    $this->clientSecret = $secret;
    return $this;
  }

  /**
   * Call the API and return the response.
   *
   * @param \Drupal\Core\Url $url
   *   API Url.
   * @param array $options
   *   Guzzle request options.
   *
   * @return bool|array
   *   Response string or false if failed.
   */
  protected function getApiResponse(Url $url, array $options = []) {
    try {
      $response = $this->client->request('GET', $url->toString(TRUE)->getGeneratedUrl(), $options);
    }
    catch (GuzzleException | \Exception $e) {
      $this->cache->delete('cap:access_token');
      // Most errors originate from the API itself, log the error and let it
      // fall over.
      $this->logger->error('An unexpected error came from the API: %message', ['%message' => $e->getMessage()]);
      throw new \Exception($e->getMessage());
    }
    return $response->getStatusCode() == 200 ? json_decode((string) $response->getBody(), TRUE, 512, JSON_THROW_ON_ERROR) : FALSE;
  }

  /**
   * {@inheritDoc}
   */
  public function getOrganizationUrl(array $organizations, bool $children = FALSE): Url {
    $organizations = preg_replace('/[^A-Z,0-9]/', '', strtoupper(implode(',', array_unique($organizations))));
    $query = ['orgCodes' => $organizations];
    if ($children) {
      $query['includeChildren'] = 'true';
    }
    return Url::fromUri(self::CAP_URL, ['query' => $query]);
  }

  /**
   * {@inheritDoc}
   */
  public function getWorkgroupUrl(array $workgroups): Url {
    $workgroups = preg_replace('/[^A-Z,:~\-_0-9]/', '', strtoupper(implode(',', array_unique($workgroups))));
    return Url::fromUri(self::CAP_URL, ['query' => ['privGroups' => $workgroups]]);
  }

  /**
   * {@inheritDoc}
   */
  public function getSunetUrl(array $sunetids): Url {
    $sunetids = array_unique($sunetids);
    $query = ['uids' => implode(',', $sunetids), 'ps' => count($sunetids)];
    return Url::fromUri(self::CAP_URL, ['query' => $query]);
  }

  /**
   * {@inheritDoc}
   */
  public function getTotalProfileCount(Url $url): int {
    $token = $this->getAccessToken();
    $url->mergeOptions(['query' => ['ps' => 1, 'access_token' => $token]]);
    $response = $this->getApiResponse($url);
    return (int) ($response['totalCount'] ?? 0);
  }

  /**
   * {@inheritDoc}
   */
  public function testConnection(): bool {
    $this->cache->invalidate('cap:access_token');
    try {
      return !empty($this->getAccessToken());
    }
    catch (\Throwable $e) {
      $this->logger->error('Unable to connect to CAP Api: %message', ['%message' => $e->getMessage()]);
    }
    return FALSE;
  }

  /**
   * {@inheritDoc}
   */
  public function updateOrganizations(): void {
    $this->insertOrgData($this->getOrgData());
  }

  /**
   * Insert the given organization data into the database.
   *
   * @param array $org_data
   *   Keyed array of organization data.
   * @param \Drupal\taxonomy\TermInterface|null $parent
   *   The organization parent if one exists.
   *
   * @throws \Exception
   */
  protected function insertOrgData(array $org_data, TermInterface $parent = NULL): void {
    if (!isset($org_data['orgCodes'])) {
      return;
    }

    $term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
    $tids = $term_storage->getQuery()
      ->accessCheck(FALSE)
      ->condition('vid', 'cap_org_codes')
      ->condition('su_cap_org_code', $org_data['orgCodes'], 'IN')
      ->execute();

    if (empty($tids)) {
      /** @var \Drupal\taxonomy\TermInterface $term */
      $term = $term_storage->create([
        'name' => $org_data['name'] . ' (' . implode(', ', $org_data['orgCodes']) . ')',
        'vid' => 'cap_org_codes',
        'su_cap_org_code' => $org_data['orgCodes'],
      ]);

      if ($parent) {
        $term->set('parent', $parent->id());
      }
      $term->save();
      $parent = $term;
    }
    else {
      $parent = $term_storage->load(reset($tids));
    }

    if (!empty($org_data['children'])) {
      foreach ($org_data['children'] as $child) {
        $this->insertOrgData($child, $parent);
      }
    }
  }

  /**
   * Get the organization data array from the API.
   *
   * @return array
   *   Keyed array of all organization data.
   */
  protected function getOrgData(): array {
    if ($cache = $this->cache->get('cap:org_data')) {
      return $cache->data;
    }

    $options = ['query' => ['access_token' => $this->getAccessToken()]];
    // AA00 is the root level of all Stanford.
    $result = $this->getApiResponse(Url::fromUri(self::API_URL . '/cap/v1/orgs/AA00', $options));

    if ($result) {
      $this->cache->set('cap:org_data', $result, time() + 60 * 60 * 24 * 7, [
        'cap',
        'cap:org-data',
      ]);
      return $result;
    }
    return [];
  }

  /**
   * Get the API token for CAP.
   *
   * @return string|null
   *   API Token.
   */
  protected function getAccessToken(): ?string {
    if ($cache = $this->cache->get('cap:access_token')) {
      return $cache->data['access_token'];
    }

    $options = ['query' => ['grant_type' => 'client_credentials']];
    $result = $this->getApiResponse(Url::fromUri(self::AUTH_URL, $options), [
      'auth' => [$this->clientId, $this->clientSecret],
    ]);
    $this->cache->set('cap:access_token', $result, time() + $result['expires_in'] - 3600, [
      'cap',
      'cap:token',
    ]);

    return $result['access_token'] ?? NULL;
  }

}