diff --git a/embargo.module b/embargo.module index c5b35dc..9abeb0b 100644 --- a/embargo.module +++ b/embargo.module @@ -7,7 +7,12 @@ use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\embargo\EmbargoInterface; +use Drupal\embargo\EmbargoItemList; use Drupal\embargo\EmbargoStorage; /** @@ -97,3 +102,32 @@ function embargo_theme($existing, $type, $theme, $path) { ], ]; } + +/** + * Implements hook_entity_base_field_info(). + */ +function embargo_entity_base_field_info(EntityTypeInterface $entity_type) { + if (!in_array($entity_type->id(), ['file', 'media', 'node'])) { + return []; + } + + $fields = []; + + $fields['embargo'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Embargoes')) + ->setDescription(t('Embargoes affecting this item.')) + ->setTranslatable(FALSE) + ->setRevisionable(FALSE) + ->setRequired(FALSE) + ->setDisplayConfigurable('view', FALSE) + ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) + ->setComputed(TRUE) + ->setClass(EmbargoItemList::class) + ->setSetting('target_type', 'embargo') + ->setSetting('embargo_types', match($entity_type->id()) { + 'file', 'media' => [EmbargoInterface::EMBARGO_TYPE_NODE, EmbargoInterface::EMBARGO_TYPE_FILE], + 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE], + }); + + return $fields; +} diff --git a/src/EmbargoItemList.php b/src/EmbargoItemList.php new file mode 100644 index 0000000..c2579d9 --- /dev/null +++ b/src/EmbargoItemList.php @@ -0,0 +1,44 @@ +getEntity(); + /** @var \Drupal\embargo\EmbargoStorageInterface $embargo_storage */ + $embargo_storage = $this->getEntityTypeManager()->getStorage('embargo'); + $this->setValue(array_filter($embargo_storage->getApplicableEmbargoes($entity), function (EmbargoInterface $embargo) { + return in_array($embargo->getEmbargoType(), $this->getSetting('embargo_types')); + })); + } + + /** + * Helper; get the entity type manager service. + * + * XXX: Dependency injection does not presently appear to be possible in typed + * data. + * + * @see https://www.drupal.org/node/2053415 + * @see https://www.drupal.org/project/drupal/issues/3294266 + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager service. + */ + protected function getEntityTypeManager() : EntityTypeManagerInterface { + return \Drupal::entityTypeManager(); + } + +} diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 325778b..fb6eefa 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -2,19 +2,15 @@ namespace Drupal\embargo\Plugin\search_api\processor; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; -use Drupal\Core\TypedData\ComplexDataDefinitionInterface; use Drupal\embargo\EmbargoInterface; -use Drupal\embargo\Plugin\search_api\processor\Property\EmbargoInfoProperty; -use Drupal\islandora\IslandoraUtils; use Drupal\search_api\Datasource\DatasourceInterface; -use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; +use Drupal\search_api\Query\ConditionGroupInterface; use Drupal\search_api\Query\QueryInterface; -use Drupal\search_api\SearchApiException; -use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -24,9 +20,9 @@ * @SearchApiProcessor( * id = "embargo_processor", * label = @Translation("Embargo access"), - * description = @Translation("Add information regarding embargo access constraints."), + * description = @Translation("Add information regarding embargo access + * constraints."), * stages = { - * "add_properties" = 20, * "pre_index_save" = 20, * "preprocess_query" = 20, * }, @@ -45,16 +41,6 @@ class EmbargoProcessor extends ProcessorPluginBase implements ContainerFactoryPl */ protected EntityTypeManagerInterface $entityTypeManager; - /** - * Islandora utils helper service. - * - * XXX: Ideally, this would reference an interface; however, such does not - * exist. - * - * @var \Drupal\islandora\IslandoraUtils - */ - protected IslandoraUtils $islandoraUtils; - /** * The request stack. * @@ -69,6 +55,13 @@ class EmbargoProcessor extends ProcessorPluginBase implements ContainerFactoryPl */ protected AccountProxyInterface $currentUser; + /** + * Drupal's time service. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + protected TimeInterface $time; + /** * {@inheritdoc} */ @@ -77,125 +70,29 @@ public static function create(ContainerInterface $container, array $configuratio $instance->requestStack = $container->get('request_stack'); $instance->entityTypeManager = $container->get('entity_type.manager'); - $instance->islandoraUtils = $container->get('islandora.utils'); $instance->currentUser = $container->get('current_user'); + $instance->time = $container->get('datetime.time'); return $instance; } - /** - * {@inheritdoc} - */ - public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array { - if ($datasource !== NULL) { - return []; - } - - return [ - 'embargo_info' => new EmbargoInfoProperty([ - 'label' => $this->t('Embargo info'), - 'description' => $this->t('Aggregated embargo info'), - 'processor_id' => $this->getPluginId(), - 'is_list' => TRUE, - 'computed' => TRUE, - ]), - ]; - } - - /** - * Get the embargo(es) associated with the given index item. - * - * @param \Drupal\search_api\Item\ItemInterface $item - * The index item to consider. - * - * @return iterable - * A generated sequence of applicable embargoes. - * - * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException - * @throws \Drupal\Core\TypedData\Exception\MissingDataException - * @throws \Drupal\search_api\SearchApiException - */ - protected function getEmbargoes(ItemInterface $item) : iterable { - try { - foreach ($this->entityTypeManager->getStorage('embargo') - ->getApplicableEmbargoes($item->getOriginalObject()->getValue()) as $embargo) { - yield $embargo; - } - } - catch (SearchApiException) { - // No-op; object probably did not exist? - } - } - /** * {@inheritDoc} */ public function preIndexSave() { parent::preIndexSave(); - foreach ($this->getPropertyDefinitions() as $base => $def) { - if ($def instanceof ComplexDataDefinitionInterface) { - foreach (array_keys($def->getPropertyDefinitions()) as $name) { - $this->ensureField(NULL, "{$base}:{$name}"); - } - } - } - } - - /** - * {@inheritdoc} - */ - public function addFieldValues(ItemInterface $item) : void { - $source_type_id = $item->getDatasource()->getEntityTypeId(); - if (!in_array($source_type_id, static::ENTITY_TYPES)) { - return; - } - - $info = [ - 'total_count' => 0, - 'indefinite_count' => 0, - 'scheduled_timestamps' => [], - 'exempt_users' => [], - 'exempt_ip_ranges' => [], - ]; - - // Get Embargo details and prepare to pass it to index field. - foreach ($this->getEmbargoes($item) as $embargo) { - if ($embargo->getEmbargoType() === EmbargoInterface::EMBARGO_TYPE_FILE && $source_type_id === 'node') { + foreach ($this->index->getDatasources() as $datasource_id => $datasource) { + if (!in_array($datasource->getEntityTypeId(), static::ENTITY_TYPES)) { continue; } - $info['total_count']++; - if ($embargo->getExpirationType() === EmbargoInterface::EXPIRATION_TYPE_INDEFINITE) { - $info['indefinite_count']++; - } - else { - $info['scheduled_timestamps'][] = $embargo->getExpirationDate()->getTimestamp(); - } - - $info['exempt_users'] = array_merge( - $info['exempt_users'], - array_map(function (UserInterface $user) { - return $user->id(); - }, $embargo->getExemptUsers()), - ); - if ($range_id = $embargo->getExemptIps()?->id()) { - $info['exempt_ip_ranges'][] = $range_id; - } - } - - foreach (['scheduled_timestamps', 'exempt_users', 'exempt_ip_ranges'] as $key) { - $info[$key] = array_unique($info[$key]); - } - - foreach ($info as $key => $val) { - if ($field = $this->findField(NULL, "embargo_info:{$key}")) { - $item_field = $item->getField($field->getFieldIdentifier(), FALSE); - foreach ((is_array($val) ? $val : [$val]) as $value) { - $item_field->addValue($value); - } - } + $this->ensureField($datasource_id, 'embargo:entity:id', 'integer'); + $this->ensureField($datasource_id, 'embargo:entity:embargo_type', 'integer'); + $this->ensureField($datasource_id, 'embargo:entity:expiration_date', 'date'); + $this->ensureField($datasource_id, 'embargo:entity:expiration_type', 'integer'); + $this->ensureField($datasource_id, 'embargo:entity:exempt_ips:entity:id', 'integer'); + $this->ensureField($datasource_id, 'embargo:entity:exempt_users:entity:uid', 'integer'); } } @@ -216,41 +113,62 @@ public function preprocessSearchQuery(QueryInterface $query) : void { return; } + $and_group = $query->createAndAddConditionGroup(tags: ['embargo_processor', 'embargo_access']); + foreach (array_keys($applicable_datasources) as $datasource_id) { + if ($filter = $this->addEmbargoFilters($datasource_id, $query)) { + $and_group->addConditionGroup($filter); + } + } + } + + /** + * Add embargo filters to the given query, for the given datasource. + * + * @param string $datasource_id + * The ID of the datasource for which to add filters. + * @param \Drupal\search_api\Query\QueryInterface $query + * The query to which to add filters. + */ + protected function addEmbargoFilters(string $datasource_id, QueryInterface $query) : ?ConditionGroupInterface { $or_group = $query->createConditionGroup('OR', [ - 'embargo_processor', - 'embargo_access', + "embargo:$datasource_id", ]); // No embargo. - if ($field = $this->findField(NULL, 'embargo_info:total_count')) { - $or_group->addCondition($field->getFieldIdentifier(), 0); + if ($field = $this->findField($datasource_id, 'embargo:entity:id')) { + $or_group->addCondition($field->getFieldIdentifier(), NULL); } // Embargo durations. // No indefinite embargo. - if ($indefinite_field = $this->findField(NULL, 'embargo_info:indefinite_count')) { - $scheduled_group = $query->createConditionGroup(tags: [ - 'embargo_scheduled', - ]); - $scheduled_group->addCondition($indefinite_field->getFieldIdentifier(), 0); - - // No scheduled embargo in the future. - // XXX: Might not quite work? If there's a single scheduled embargo - // lesser, would it open it? - if ($scheduled_field = $this->findField(NULL, 'embargo_info:scheduled_timestamps')) { - $scheduled_group->addCondition($scheduled_field->getFieldIdentifier(), [ - 0 => time() + 1, - 1 => PHP_INT_MAX, + if ($expiration_type_field = $this->findField($datasource_id, 'embargo:entity:expiration_type')) { + $or_group->addCondition($expiration_type_field->getFieldIdentifier(), EmbargoInterface::EXPIRATION_TYPE_INDEFINITE, '<>'); + + // Scheduled embargo in the past and none in the future. + if ($scheduled_field = $this->findField($datasource_id, 'embargo:entity:expiration_date')) { + $schedule_group = $query->createConditionGroup(tags: ['embargo_scheduled']); + + $schedule_group->addCondition($expiration_type_field->getFieldIdentifier(), EmbargoInterface::EXPIRATION_TYPE_SCHEDULED); + // Embargo in the past. + $schedule_group->addCondition($scheduled_field->getFieldIdentifier(), date('Y-m-d', $this->time->getRequestTime()), '<='); + // No embargo in the future. + $schedule_group->addCondition($scheduled_field->getFieldIdentifier(), [ + 0 => date('Y-m-d', strtotime('+1 DAY', $this->time->getRequestTime())), + 1 => date('Y-m-d', PHP_INT_MAX), ], 'NOT BETWEEN'); + + $or_group->addConditionGroup($schedule_group); } - $or_group->addConditionGroup($scheduled_group); } - if (!$this->currentUser->isAnonymous() && ($field = $this->findField(NULL, 'embargo_info:exempt_users'))) { + if ( + !$this->currentUser->isAnonymous() && + ($field = $this->findField($datasource_id, 'embargo:entity:exempt_users:entity:uid')) + ) { $or_group->addCondition($field->getFieldIdentifier(), $this->currentUser->id()); } - if ($field = $this->findField(NULL, 'embargo_info:exempt_ip_ranges')) { + if ($field = $this->findField($datasource_id, 'embargo:entity:exempt_ips:entity:id')) { /** @var \Drupal\embargo\IpRangeStorageInterface $ip_range_storage */ $ip_range_storage = $this->entityTypeManager->getStorage('embargo_ip_range'); foreach ($ip_range_storage->getApplicableIpRanges($this->requestStack->getCurrentRequest() @@ -259,9 +177,7 @@ public function preprocessSearchQuery(QueryInterface $query) : void { } } - if (count($or_group->getConditions()) > 0) { - $query->addConditionGroup($or_group); - } + return (count($or_group->getConditions()) > 0) ? $or_group : NULL; } } diff --git a/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php b/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php deleted file mode 100644 index 802aafe..0000000 --- a/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php +++ /dev/null @@ -1,90 +0,0 @@ -propertyDefinitions)) { - $this->propertyDefinitions = [ - 'total_count' => new ProcessorProperty([ - 'label' => $this->t('Applicable embargo count'), - 'description' => $this->t('Total number of applicable embargoes.'), - 'type' => 'integer', - 'processor_id' => $this->getProcessorId(), - 'is_list' => FALSE, - 'computed' => TRUE, - ]), - 'indefinite_count' => new ProcessorProperty([ - 'label' => $this->t('Count of indefinite embargoes'), - 'description' => $this->t('Total number of indefinite embargoes.'), - 'type' => 'integer', - 'processor_id' => $this->getProcessorId(), - 'is_list' => FALSE, - 'computed' => TRUE, - ]), - 'scheduled_timestamps' => new ProcessorProperty([ - 'label' => $this->t('Schedule embargo expiry timestamps'), - 'description' => $this->t('Unix timestamps when the embargoes expire.'), - 'type' => 'integer', - 'processor_id' => $this->getProcessorId(), - 'is_list' => TRUE, - 'computed' => TRUE, - ]), - 'exempt_users' => new ProcessorProperty([ - 'label' => $this->t('IDs of users exempt from embargoes'), - 'description' => '', - 'type' => 'integer', - 'processor_id' => $this->getProcessorId(), - 'is_list' => TRUE, - 'computed' => TRUE, - ]), - 'exempt_ip_ranges' => new ProcessorProperty([ - 'label' => $this->t('IP range entity IDs'), - 'description' => '', - 'type' => 'string', - 'processor_id' => $this->getProcessorId(), - 'is_list' => TRUE, - 'computed' => TRUE, - ]), - ]; - } - - return $this->propertyDefinitions; - } - - /** - * {@inheritDoc} - */ - public function getProcessorId() { - return $this->definition['processor_id']; - } - - /** - * {@inheritDoc} - */ - public function isHidden() : bool { - return !empty($this->definition['hidden']); - } - - /** - * {@inheritdoc} - */ - public function isList() : bool { - return (bool) ($this->definition['is_list'] ?? parent::isList()); - } - -}