Drupal Search API - Media Access - Custom Processor Plugin

By admin, 17 October, 2023

The MediaAccess custom processor plugin is a testament to Drupal's flexibility, offering enhanced media access controls during the indexing and querying processes through the Search API and Permissions by Term Module

<?php
namespace Drupal\custom_module\Plugin\search_api\processor;
use Drupal\Core\Database\Connection;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\Session\AnonymousUserSession;
use Drupal\Core\TypedData\ComplexDataInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\IndexInterface;
use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\LoggerTrait;
use Drupal\search_api\Processor\ProcessorPluginBase;
use Drupal\search_api\Processor\ProcessorProperty;
use Drupal\search_api\Query\QueryInterface;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Adds media access checks for media
 *
 * @SearchApiProcessor(
 *   id = "media_access",
 *   label = @Translation("Media access"),
 *   description = @Translation("Adds media access checks for media type"),
 *   stages = {
 *     "add_properties" = 0,
 *     "pre_index_save" = -10,
 *     "preprocess_query" = -30,
 *   },
 * )
 */
class MediaAccess extends ProcessorPluginBase {
  use LoggerTrait;
  /**
   * The database connection.
   *
   * @var \Drupal\Core\Database\Connection|null
   */
  protected $database;
  /**
   * The current_user service used by this plugin.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface|null
   */
  protected $currentUser;
  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    /** @var static $processor */
    $processor = parent::create($container, $configuration, $plugin_id, $plugin_definition);
    $processor->setLogger($container->get('logger.channel.search_api'));
    $processor->setDatabase($container->get('database'));
    $processor->setCurrentUser($container->get('current_user'));
    return $processor;
  }

  /**
   * Retrieves the database connection.
   *
   * @return \Drupal\Core\Database\Connection
   *   The database connection.
   */
  public function getDatabase() {
    return $this->database ?: \Drupal::database();
  }
  /**
   * Sets the database connection.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The new database connection.
   *
   * @return $this
   */
  public function setDatabase(Connection $database) {
    $this->database = $database;
    return $this;
  }
  /**
   * Retrieves the current user.
   *
   * @return \Drupal\Core\Session\AccountProxyInterface
   *   The current user.
   */
  public function getCurrentUser() {
    return $this->currentUser ?: \Drupal::currentUser();
  }
  /**
   * Sets the current user.
   *
   * @param \Drupal\Core\Session\AccountProxyInterface $current_user
   *   The current user.
   *
   * @return $this
   */
  public function setCurrentUser(AccountProxyInterface $current_user) {
    $this->currentUser = $current_user;
    return $this;
  }

  /**
   * {@inheritdoc}
   */
  public static function supportsIndex(IndexInterface $index) {
    foreach ($index->getDatasources() as $datasource) {
      if (in_array($datasource->getEntityTypeId(), ['media'])) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
    $properties = [];
    if (!$datasource) {
      $definition = [
        'label' => $this->t('Media access information'),
        'description' => $this->t('Data needed to apply media access.'),
        'type' => 'string',
        'processor_id' => $this->getPluginId(),
        'hidden' => TRUE,
        'is_list' => TRUE,
      ];
      $properties['search_api_media_grants'] = new ProcessorProperty($definition);
    }
    return $properties;
  }

  /**
   * {@inheritdoc}
   */
  public function addFieldValues(ItemInterface $item) {
    static $anonymous_user;
    if (!isset($anonymous_user)) {
      // Load the anonymous user.
      $anonymous_user = new AnonymousUserSession();
    }
    // Only run for media items.
    $entity_type_id = $item->getDatasource()->getEntityTypeId();
    if (!in_array($entity_type_id, ['media'])) {
      return;
    }
    // Get the media object.
    $media = $this->getMedia($item->getOriginalObject());
    if (!$media) {
      // Apparently we were active for a wrong item.
      return;
    }
    $fields = $item->getFields();
    $fields = $this->getFieldsHelper()
      ->filterForPropertyPath($fields, NULL, 'search_api_media_grants');
    $media_access_terms = [];
    $media_access_terms = $media->field_media_access_terms->referencedEntities();
    $field = reset($fields);
    if (!empty($media_access_terms)) {
      foreach ($media_access_terms as $term) {
         $field->addValue('media_access_' . $term->id());
      }
    } else {
      // Add the generic pseudo view grant if we are not using media access.
      $field->addValue('media_access__all');
    }
  }

  /**
   * {@inheritdoc}
   */
  public function preIndexSave() {
    foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
      $entity_type = $datasource->getEntityTypeId();
      if (in_array($entity_type, ['media'])) {
        $this->ensureField($datasource_id, 'status', 'boolean');
      }
    }
    $field = $this->ensureField(NULL, 'search_api_media_grants', 'string');
    $field->setHidden();
  }

  /**
   * Retrieves the media related to an indexed search object.
   *
   * Will be  the media itself.
   *
   * @param \Drupal\Core\TypedData\ComplexDataInterface $item
   *   A search object that is being indexed.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   The media related to that search object.
   */
  protected function getMedia(ComplexDataInterface $item) {
    $item = $item->getValue();
    if ($item instanceof EntityInterface) {
      return $item;
    }
    return NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function preprocessSearchQuery(QueryInterface $query) {
    if (!$query->getOption('search_api_bypass_access')) {
      $account = $query->getOption('search_api_access_account', $this->getCurrentUser());
      if (is_numeric($account)) {
        $account = User::load($account);
      }
      if ($account instanceof AccountInterface) {
        $this->addMediaAccess($query, $account);
      } else {
        $account = $query->getOption('search_api_access_account', $this->getCurrentUser());
        if ($account instanceof AccountInterface) {
          $account = $account->id();
        }
        if (!is_scalar($account)) {
          $account = var_export($account, TRUE);
        }
        $this->getLogger()->warning('An illegal user UID was given for media access: @uid.', ['@uid' => $account]);
      }
    }
  }

  /**
   * Adds a Media access filter to a search query, if applicable.
   *
   * @param \Drupal\search_api\Query\QueryInterface $query
   *   The query to which a media access filter should be added, if applicable.
   * @param \Drupal\Core\Session\AccountInterface $account
   *   The user for whom the search is executed.
   */
  protected function addMediaAccess(QueryInterface $query, AccountInterface $account) {
    // Don't do anything if the user can access all content.
    if ($account->hasPermission('bypass media access')) {
      return;
    }
    $access_conditions = $query;
    $access_storage = \Drupal::service('permissions_by_term.access_storage');
    $user_media_access_terms = $access_storage->getPermittedTids($account->id(), $account->getRoles());
    // Filter by the user's media access grants.
    $media_grants_field = $this->findField(NULL, 'search_api_media_grants', 'string');
    if (!$media_grants_field) {
      return;
    }
    $media_grants_field_id = $media_grants_field->getFieldIdentifier();
    $grants_conditions = $query->createConditionGroup('OR', ['media_access_grants']);
    if (isset($user_media_access_terms)) {
      foreach ($user_media_access_terms as $term) {
        $grants_conditions->addCondition($media_grants_field_id, 'media_access' . $term;
      }
    }
    // Also add items that are accessible for everyone by checking the "access all" pseudo grant.
    $grants_conditions->addCondition($media_grants_field_id, 'media_access__all');
    $access_conditions->addConditionGroup($grants_conditions);
  }
}

References: The content access processor plugin from the search API contrib module.

 1. web/modules/contrib/search_api/src/Plugin/search_api/processor/ContentAccess.php

Pre-requisites: Knowledge of Drupal Framework and its Components.

Audience: Drupal Developers

Stars
No votes yet