coveo-1.0.0-alpha1/modules/coveo_secured_search/src/Plugin/search_api/backend/CoveoIdentityBackend.php

modules/coveo_secured_search/src/Plugin/search_api/backend/CoveoIdentityBackend.php
<?php

declare(strict_types=1);

namespace Drupal\coveo_secured_search\Plugin\search_api\backend;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\coveo\Entity\CoveoOrganizationInterface;
use Drupal\coveo\Plugin\CoveoSecurityProviderManagerInterface;
use Drupal\coveo_secured_search\Event\CoveoIdentitiesAlter;
use Drupal\coveo_secured_search\Plugin\CoveoCustomSecurityProviderPluginInterface;
use Drupal\search_api\Backend\BackendPluginBase;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Plugin\PluginFormTrait;
use Drupal\search_api\Query\QueryInterface;
use NecLimDul\Coveo\PushApi\Api\SecurityIdentityApi;
use NecLimDul\Coveo\PushApi\Model\BaseIdentityBody;
use NecLimDul\Coveo\PushApi\Model\Identity;
use NecLimDul\Coveo\PushApi\Model\IdentityBody;
use Neclimdul\OpenapiPhp\Helper\Logging\Error;
use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Coveo search API backend plugin.
 *
 * @SearchApiBackend(
 *   id = "coveo_identity",
 *   label = @Translation("Coveo Identity Provider"),
 *   description = @Translation("Index identity items in Coveo. IMPORTANT: The SearchAPI item ID will be the identity and all fields will be ignored."),
 * )
 */
class CoveoIdentityBackend extends BackendPluginBase implements PluginFormInterface {

  use PluginFormTrait;

  /**
   * Constructs a Coveo Identity Search API backend.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    array $plugin_definition,
    LoggerInterface $logger,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly EventDispatcherInterface $eventDispatcher,
    private readonly TimeInterface $time,
    private readonly CoveoSecurityProviderManagerInterface $securityProviderManager,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->setLogger($logger);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    $plugin = new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('logger.channel.coveo'),
      $container->get('entity_type.manager'),
      $container->get('event_dispatcher'),
      $container->get('datetime.time'),
      $container->get('plugin.manager.coveo_security_provider'),
    );

    // @todo correctly set these up in the constructor.
    $plugin->setFieldsHelper($container->get('search_api.fields_helper'));
    $plugin->setMessenger($container->get('messenger'));
    $plugin->setStringTranslation($container->get('string_translation'));

    return $plugin;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration(): array {
    return [
      'organization_name' => '',
      'provider_id' => '',
      'identity_type' => 'USER',
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
    $form['organization_name'] = [
      '#type' => 'select',
      '#title' => 'Organization',
      '#default_value' => $this->getOrganizationName(),
      '#options' => array_map(
        fn(CoveoOrganizationInterface $org) => $org->label(),
        $this->getOrganizations()
      ),
      '#required' => TRUE,
    ];
    $form['provider_id'] = [
      '#type' => 'select',
      '#title' => $this->t('Security Provider'),
      // @todo link to creating a security provider to help lost users.
      '#description' => $this->t('The custom security provider that will connected to the pushed identity.'),
      '#default_value' => $this->getProviderPluginId(),
      '#options' => $this->getSecurityProviders(),
      '#required' => TRUE,
    ];
    $form['identity_type'] = [
      '#type' => 'select',
      '#title' => $this->t('Identity Type'),
      // @todo find some real documentation to link to. It's hidden in swagger but that's not clear.
      '#description' => $this->t('The type of identity being pushed.'),
      '#default_value' => $this->getIdentityType(),
      '#options' => $this->getIdentityTypes(),
      '#required' => TRUE,
    ];

    return $form;
  }

  /**
   * Get a list of identity types keyed by the internal id.
   *
   * @return array<string,string>
   *   List security provider options.
   */
  private function getSecurityProviders(): array {
    return array_map(
      fn($definition) => $definition['title'],
      array_filter(
        $this->securityProviderManager->getDefinitions(),
        fn(array $definition) => $definition['category'] === 'custom',
      ),
    );
  }

  /**
   * Get a list of identity types keyed by the internal id.
   *
   * @return array<string, \Drupal\Core\StringTranslation\TranslatableMarkup|string>
   *   List identity type options.
   */
  private function getIdentityTypes(): array {
    return [
      'USER' => $this->t('User'),
      'GROUP' => $this->t('Group'),
      'VIRTUAL_GROUP' => $this->t('Virtual Group'),
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function validateConfigurationForm(array &$form, FormStateInterface $form_state): void {
    // @todo validate the values allow us to connect to an API.
  }

  /**
   * {@inheritdoc}
   */
  public function viewSettings(): array {
    $info = [];
    $org = $this->getOrganization();
    $info[] = [
      'label' => $this->t('Organization'),
      'info' => Link::createFromRoute(
        $org?->label(),
        'entity.coveo_organization.view',
        ['coveo_organization' => $org?->id()],
      ),
    ];
    $info[] = [
      'label' => $this->t('Security Provider'),
      'info' => $this->getSecurityProviders()[$this->getProviderPluginId()] ?? 'Broken',
    ];
    $info[] = [
      'label' => $this->t('Identity type'),
      'info' => $this->getIdentityTypes()[$this->getIdentityType()],
    ];
    // @todo audit indexes to make sure they conform to our hacky field mapping logic.
    return $info;
  }

  /**
   * {@inheritdoc}
   */
  public function getSupportedFeatures(): array {
    return [];
  }

  /**
   * {@inheritdoc}
   */
  public function indexItems(IndexInterface $index, array $items) {
    // If read only, just tell SearchAPI we finished without sending anything.
    if ($this->isReadOnly()) {
      return array_keys($items);
    }

    $identities = [];
    foreach ($items as $key => $item) {
      $identities[$key] = new IdentityBody([
        'identity' => new Identity([
          'name' => $this->getSecurityProvider()->getNameFromId($key),
          'type' => $this->getIdentityType(),
        ]),
      ]);
    }

    // Let other modules alter identities before sending them to Coveo.
    // This is a great place for them to interact with more complex features.
    $this->eventDispatcher->dispatch(new CoveoIdentitiesAlter($identities, $items, $index));

    if (count($identities) > 0) {
      $api = $this->getOrganization()->pushApiCreate(SecurityIdentityApi::class);
      $provider_id = $this->getIdentityProviderId();
      $organization_id = $this->getOrganizationId();
      // @todo Probably better if this was re-written to batch.
      foreach ($identities as $identity) {
        $response = $api->organizationsOrganizationIdProvidersProviderIdPermissionsPut(
          $provider_id,
          $organization_id,
          $identity,
        );
        if (!$response->isSuccess()) {
          // $this->getLogger()->error('Something wrong here. Log it.');
          Error::logError(
            $this->getLogger(),
            $response,
            LogLevel::ERROR,
            'Identity item index error: ' . Error::DEFAULT_ERROR_MESSAGE,
          );
        }
      }
    }

    return array_keys($identities);
  }

  /**
   * {@inheritdoc}
   */
  public function deleteItems(IndexInterface $index, array $item_ids) {
    // If read only, just tell SearchAPI we finished without sending anything.
    if ($this->isReadOnly()) {
      return;
    }

    $organization = $this->getOrganization();
    $api = $organization->pushApiCreate(SecurityIdentityApi::class);
    $provider_id = $this->getIdentityProviderId();
    foreach ($item_ids as $id) {
      // @todo This doesn't actually work because the ID could be something
      //   other then the item ID (some field value) and the type is arbitrary.
      $this->getIdentityProviderId();
      $response = $api->organizationsOrganizationIdProvidersProviderIdPermissionsDelete(
        $provider_id,
        $this->getOrganizationId(),
        new BaseIdentityBody([
          'identity' => new Identity([
            'name' => $this->getSecurityProvider()->getNameFromId($id),
            'type' => $this->getIdentityType(),
          ]),
        ]),
      );
      if (!$response->isSuccess()) {
        // $this->getLogger()->error('Something wrong here. Log it.');
        Error::logError(
          $this->getLogger(),
          $response,
          LogLevel::ERROR,
          'Identity item delete error: ' . Error::DEFAULT_ERROR_MESSAGE,
        );
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAllIndexItems(?IndexInterface $index = NULL, $datasource_id = NULL): void {
    if ($index && !$this->isReadOnly()) {
      $api = $this->getOrganization()->pushApiCreate(SecurityIdentityApi::class);
      $provider_id = $this->getIdentityProviderId();
      $api->organizationsOrganizationIdProvidersProviderIdPermissionsOlderthanDelete(
        $provider_id,
        $this->getOrganizationId(),
        // Convert current time to microseconds. We don't use
        // getCurrentMicroTime so we can round to avoid any server clock skew
        // creating a value in the future.
        (int) (1000 * $this->time->getCurrentTime()),
        // Shorter queue delay to avoid confusion. 15min might take unexpectedly
        // long.
        1
      );
    }
  }

  /**
   * {@inheritdoc}
   */
  public function search(QueryInterface $query): void {
    // Not searchable...
  }

  /**
   * Get the organization configuration machine name.
   */
  public function getOrganizationName() {
    return $this->configuration['organization_name'];
  }

  /**
   * Get the security provider plugin id.
   */
  public function getProviderPluginId() {
    return $this->configuration['provider_id'];
  }

  /**
   * Check if backend is in a read only state.
   *
   * @return bool
   *   True if the backend is in a read only state.
   */
  public function isReadOnly(): bool {
    // If read only, just tell SearchAPI we finished without sending anything.
    $organization = $this->getOrganization();
    // Check if organization is null to be safe. Some edge cases during config
    // import can lead to this happening.
    return $organization === NULL || $organization->isReadOnly();
  }

  /**
   * Get the associated security provider ID.
   *
   * @return string
   *   The security provider ID used to communicated with Coveo.
   */
  public function getIdentityProviderId(): string {
    return $this->getSecurityProvider()->getIdentityProviderId();
  }

  /**
   * Get the identity type that will be pushed to coveo.
   */
  private function getIdentityType() {
    return $this->configuration['identity_type'];
  }

  /**
   * Get the associated security provider plugin.
   *
   * @return \Drupal\coveo_secured_search\Plugin\CoveoCustomSecurityProviderPluginInterface
   *   The security provider plugin.
   */
  public function getSecurityProvider(): CoveoCustomSecurityProviderPluginInterface {
    return $this
      ->securityProviderManager
      ->createInstance($this->getProviderPluginId())
      ->getCustomSecurityProviderPlugin();
  }

  /**
   * Get the organization ID (provided by Coveo).
   */
  public function getOrganizationId(): string|null {
    return $this->getOrganization()?->getOrganizationId();
  }

  /**
   * Retrieves all available Coveo organizations.
   *
   * @return \Drupal\coveo\Entity\CoveoOrganizationInterface[]
   *   The available organizations.
   */
  private function getOrganizations(): array {
    try {
      return $this->entityTypeManager
        ->getStorage('coveo_organization')
        ->loadMultiple();
    }
    catch (InvalidPluginDefinitionException | PluginNotFoundException) {
      // This should never happen.
      return [];
    }
  }

  /**
   * Get the organization config associated with the identity backend.
   */
  public function getOrganization(): CoveoOrganizationInterface|null {
    $id = $this->getOrganizationName();
    if ($id) {
      return $this->entityTypeManager
        ->getStorage('coveo_organization')
        ->load($id);
    }
    return NULL;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc