diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml
index 0589725..bac290c 100644
--- a/.github/workflows/phpunit.yml
+++ b/.github/workflows/phpunit.yml
@@ -10,3 +10,10 @@ jobs:
PHPUnit:
uses: discoverygarden/phpunit-action/.github/workflows/phpunit.yml@v1
secrets: inherit
+ with:
+ composer_patches: |-
+ {
+ "discoverygarden/islandora_hierarchical_access": {
+ "dependent work from dependency": "https://github.com/discoverygarden/islandora_hierarchical_access/pull/19.patch"
+ }
+ }
diff --git a/README.md b/README.md
index 06eef40..aab5cab 100644
--- a/README.md
+++ b/README.md
@@ -19,25 +19,50 @@ See [the module's docs for more info](modules/migrate_embargoes_to_embargo/READM
Configuration options can be set at `admin/config/content/embargo`,
including a contact email and notification message.
-Embargoes can be managed at `admin/content/embargo`.
+Embargoes can be managed at `admin/content/embargo`.
To add an IP range for use on embargoes, navigate to
`admin/content/embargo/range` and click 'Add IP range'. Ranges
created via this method can then be used as IP address whitelists when creating
-embargoes. This [CIDR to IPv4 Conversion utility](https://www.ipaddressguide.com/cidr)
+embargoes. This [CIDR to IPv4 Conversion utility](https://www.ipaddressguide.com/cidr)
can be helpful in creating valid CIDR IP ranges.
+### `search_api` processor(s)
+
+We have multiple `search_api` processors which attempt to constrain search
+results based on the effects of embargoes on the entities represented by search
+results, including:
+
+- `embargo_processor` ("Embargo access (deprecated)")
+ - Adds additional properties to the indexed rows, requiring additional index
+ maintenance on mutation of the entities under consideration, but should
+ theoretically work with any `search_api` backend
+- `embargo_join_process` ("Embargo access, join-wise")
+ - Requires Solr/Solarium-compatible index, and indexing of embargo entities in
+ the same index as the node/media/files to be search, tracking necessary info
+ and performing
+ [Solr joins](https://solr.apache.org/guide/solr/latest/query-guide/join-query-parser.html)
+ to constrain results
+
+Typically, only one should be used in any particular index.
+
## Usage
### Applying an embargo
-An embargo can be applied to an existing node by clicking the
-"Embargoes" tab on a node, or navigating to
+An embargo can be applied to an existing node by clicking the
+"Embargoes" tab on a node, or navigating to
`embargoes/node/{node_id}`. From here, an embargo can be applied if it doesn't
already exist, and existing embargoes can be modified or removed.
-## Known Issues
-Embargoed items may show up in search results. To work around this at a cost to performance you can enable access checking in your search views.
+## Known Issues/FAQ
+
+- Embargoed items show up in search results
+ - Enable one of our `search_api` processors to handle applying embargo restrictions.
+- "Embargo access, join-wise" does not show up as an available processor
+ - Ensure embargo entities are being indexed in the given index.
+ - Ensure that eligible node/media/files entities are being indexed in the
+ given index.
## Troubleshooting/Issues
diff --git a/embargo.module b/embargo.module
index c5b35dc..7dd6a09 100644
--- a/embargo.module
+++ b/embargo.module
@@ -5,15 +5,18 @@
* Hook implementations.
*/
+use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\embargo\EmbargoStorage;
+use Drupal\media\MediaInterface;
+use Drupal\node\NodeInterface;
/**
* Implements hook_entity_type_alter().
*/
-function embargo_entity_type_alter(array &$entity_types) {
+function embargo_entity_type_alter(array &$entity_types) : void {
$applicable_entity_types = EmbargoStorage::applicableEntityTypes();
foreach ($applicable_entity_types as $entity_type_id) {
$entity_type = &$entity_types[$entity_type_id];
@@ -24,7 +27,7 @@ function embargo_entity_type_alter(array &$entity_types) {
/**
* Implements hook_entity_access().
*/
-function embargo_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
+function embargo_entity_access(EntityInterface $entity, $operation, AccountInterface $account) : AccessResultInterface {
/** @var \Drupal\embargo\Access\EmbargoAccessCheckInterface $service */
$service = \Drupal::service('access_check.embargo');
return $service->access($entity, $account);
@@ -33,7 +36,7 @@ function embargo_entity_access(EntityInterface $entity, $operation, AccountInter
/**
* Implements hook_file_download().
*/
-function embargo_file_download($uri) {
+function embargo_file_download($uri) : null|array|int {
$files = \Drupal::entityTypeManager()
->getStorage('file')
->loadByProperties(['uri' => $uri]);
@@ -44,39 +47,23 @@ function embargo_file_download($uri) {
return -1;
}
}
-}
-/**
- * Implements hook_query_TAG_alter() for `node_access` tagged queries.
- */
-function embargo_query_node_access_alter(AlterableInterface $query) {
- /** @var \Drupal\embargo\Access\QueryTagger $tagger */
- $tagger = \Drupal::service('embargo.query_tagger');
- $tagger->tagAccess($query, 'node');
-}
-
-/**
- * Implements hook_query_TAG_alter() for `media_access` tagged queries.
- */
-function embargo_query_media_access_alter(AlterableInterface $query) {
- /** @var \Drupal\embargo\Access\QueryTagger $tagger */
- $tagger = \Drupal::service('embargo.query_tagger');
- $tagger->tagAccess($query, 'media');
+ return NULL;
}
/**
- * Implements hook_query_TAG_alter() for `file_access` tagged queries.
+ * Implements hook_query_TAG_alter() for `node_access` tagged queries.
*/
-function embargo_query_file_access_alter(AlterableInterface $query) {
+function embargo_query_node_access_alter(AlterableInterface $query) : void {
/** @var \Drupal\embargo\Access\QueryTagger $tagger */
$tagger = \Drupal::service('embargo.query_tagger');
- $tagger->tagAccess($query, 'file');
+ $tagger->tagNode($query);
}
/**
* Implements hook_theme().
*/
-function embargo_theme($existing, $type, $theme, $path) {
+function embargo_theme($existing, $type, $theme, $path) : array {
return [
'embargo_ip_access_exemption' => [
'template' => 'embargo-ip-access-exemption',
@@ -97,3 +84,73 @@ function embargo_theme($existing, $type, $theme, $path) {
],
];
}
+
+/**
+ * Implements hook_ENTITY_TYPE_insert() for embargo entities.
+ */
+function embargo_embargo_insert(EntityInterface $entity) : void {
+ /** @var \Drupal\embargo\SearchApiTracker $tracker */
+ $tracker = \Drupal::service('embargo.search_api_tracker_helper');
+ $tracker->track($entity);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update() for embargo entities.
+ */
+function embargo_embargo_update(EntityInterface $entity) : void {
+ /** @var \Drupal\embargo\SearchApiTracker $tracker */
+ $tracker = \Drupal::service('embargo.search_api_tracker_helper');
+ $tracker->track($entity);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_delete() for embargo entities.
+ */
+function embargo_embargo_delete(EntityInterface $entity) : void {
+ /** @var \Drupal\embargo\SearchApiTracker $tracker */
+ $tracker = \Drupal::service('embargo.search_api_tracker_helper');
+ $tracker->track($entity);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_delete() for node entities.
+ */
+function embargo_node_delete(EntityInterface $entity) : void {
+ assert($entity instanceof NodeInterface);
+ /** @var \Drupal\embargo\SearchApiTracker $tracker */
+ $tracker = \Drupal::service('embargo.search_api_tracker_helper');
+ $tracker->propagateChildren($entity);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_insert() for media entities.
+ */
+function embargo_media_insert(EntityInterface $entity) : void {
+ assert($entity instanceof MediaInterface);
+
+ /** @var \Drupal\embargo\SearchApiTracker $tracker */
+ $tracker = \Drupal::service('embargo.search_api_tracker_helper');
+ $tracker->mediaWriteReaction($entity);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_update() for media entities.
+ */
+function embargo_media_update(EntityInterface $entity) : void {
+ assert($entity instanceof MediaInterface);
+
+ /** @var \Drupal\embargo\SearchApiTracker $tracker */
+ $tracker = \Drupal::service('embargo.search_api_tracker_helper');
+ $tracker->mediaWriteReaction($entity);
+}
+
+/**
+ * Implements hook_ENTITY_TYPE_delete() for media entities.
+ */
+function embargo_media_delete(EntityInterface $entity) : void {
+ assert($entity instanceof MediaInterface);
+
+ /** @var \Drupal\embargo\SearchApiTracker $tracker */
+ $tracker = \Drupal::service('embargo.search_api_tracker_helper');
+ $tracker->mediaDeleteReaction($entity);
+}
diff --git a/embargo.services.yml b/embargo.services.yml
index 38b4738..4bcd728 100644
--- a/embargo.services.yml
+++ b/embargo.services.yml
@@ -18,11 +18,12 @@ services:
- '@entity_type.manager'
- '@datetime.time'
- '@date.formatter'
+ - '@event_dispatcher'
embargo.route_subscriber:
class: Drupal\embargo\Routing\EmbargoRouteSubscriber
arguments: ['@entity_type.manager']
tags:
- - { name: event_subscriber }
+ - { name: 'event_subscriber' }
embargo.ip_range_redirect:
class: '\Drupal\embargo\EventSubscriber\IpRangeRedirect'
arguments:
@@ -31,3 +32,40 @@ services:
- '@url_generator'
tags:
- { name: 'event_subscriber' }
+ embargo.event_subscriber.islandora_hierarchical_access:
+ class: Drupal\embargo\EventSubscriber\IslandoraHierarchicalAccessEventSubscriber
+ factory: [null, 'create']
+ arguments:
+ - '@service_container'
+ tags:
+ - { name: 'event_subscriber' }
+ embargo.search_api_tracker_helper:
+ class: Drupal\embargo\SearchApiTracker
+ factory: [null, 'create']
+ arguments:
+ - '@service_container'
+ embargo.search_api_solr_join_processor_event_subscriber:
+ class: Drupal\embargo\EventSubscriber\EmbargoJoinProcessorEventSubscriber
+ factory: [null, 'create']
+ arguments:
+ - '@service_container'
+ tags:
+ - { name: 'event_subscriber' }
+ cache_context.ip.embargo_range:
+ class: Drupal\embargo\Cache\Context\IpRangeCacheContext
+ arguments:
+ - '@request_stack'
+ - '@entity_type.manager'
+ tags:
+ - { name: 'cache.context' }
+ embargo.tagging_event_subscriber:
+ class: Drupal\embargo\EventSubscriber\TaggingEventSubscriber
+ tags:
+ - { name: 'event_subscriber' }
+ cache_context.user.embargo__has_exemption:
+ class: Drupal\embargo\Cache\Context\UserExemptedCacheContext
+ arguments:
+ - '@current_user'
+ - '@entity_type.manager'
+ tags:
+ - { name: 'cache.context' }
diff --git a/embargo.views.inc b/embargo.views.inc
new file mode 100644
index 0000000..5128bc5
--- /dev/null
+++ b/embargo.views.inc
@@ -0,0 +1,39 @@
+ \t('Embargoes'),
+ 'help' => \t('Embargoes applicable to the given node.'),
+ 'relationship' => [
+ 'base' => 'embargo',
+ 'base field' => 'embargoed_node',
+ 'field' => 'nid',
+ 'id' => 'standard',
+ 'label' => \t('Embargoes'),
+ ],
+ ];
+ $data['users_field_data']['embargo__exempt_users'] = [
+ 'title' => \t('Embargo exemptions'),
+ 'help' => \t('Embargoes for which the given user is specifically exempt.'),
+ 'relationship' => [
+ 'id' => 'entity_reverse',
+ 'field_name' => 'embargo__exempt_users',
+ 'entity_type' => 'embargo',
+ 'field table' => 'embargo__exempt_users',
+ 'field field' => 'exempt_users_target_id',
+ 'base' => 'embargo',
+ 'base field' => 'id',
+ 'label' => \t('Embargo exemptions'),
+ ],
+ ];
+
+}
diff --git a/modules/migrate_embargoes_to_embargo/src/Plugin/migrate/source/Entity.php b/modules/migrate_embargoes_to_embargo/src/Plugin/migrate/source/Entity.php
index 19756df..9ba8ddc 100644
--- a/modules/migrate_embargoes_to_embargo/src/Plugin/migrate/source/Entity.php
+++ b/modules/migrate_embargoes_to_embargo/src/Plugin/migrate/source/Entity.php
@@ -2,13 +2,11 @@
namespace Drupal\migrate_embargoes_to_embargo\Plugin\migrate\source;
-use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
-use Drupal\migrate\Plugin\MigrationInterface;
-
-use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
-
+use Drupal\migrate\Plugin\migrate\source\SourcePluginBase;
+use Drupal\migrate\Plugin\MigrationInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
@@ -42,7 +40,7 @@ public function __construct(
$plugin_id,
$plugin_definition,
MigrationInterface $migration,
- EntityTypeManagerInterface $entity_type_manager
+ EntityTypeManagerInterface $entity_type_manager,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
@@ -58,7 +56,7 @@ public static function create(
array $configuration,
$plugin_id,
$plugin_definition,
- MigrationInterface $migration = NULL
+ MigrationInterface $migration = NULL,
) {
return new static(
$configuration,
diff --git a/src/Access/EmbargoAccessCheck.php b/src/Access/EmbargoAccessCheck.php
index e643958..e1d0d1d 100644
--- a/src/Access/EmbargoAccessCheck.php
+++ b/src/Access/EmbargoAccessCheck.php
@@ -57,24 +57,40 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Req
* {@inheritdoc}
*/
public function access(EntityInterface $entity, AccountInterface $user) {
+ $type = $this->entityTypeManager->getDefinition('embargo');
+ $state = AccessResult::neutral();
+
+ if ($user->hasPermission('bypass embargo access')) {
+ return $state->setReason('User has embargo bypass permission.')
+ ->addCacheContexts(['user.permissions']);
+ }
+
/** @var \Drupal\embargo\EmbargoStorage $storage */
$storage = $this->entityTypeManager->getStorage('embargo');
+ $state->addCacheTags($type->getListCacheTags())
+ ->addCacheContexts($type->getListCacheContexts());
+ $related_embargoes = $storage->getApplicableEmbargoes($entity);
+ if (empty($related_embargoes)) {
+ return $state->setReason('No embargo statements for the given entity.');
+ }
+
+ array_map([$state, 'addCacheableDependency'], $related_embargoes);
+
$embargoes = $storage->getApplicableNonExemptNonExpiredEmbargoes(
$entity,
$this->request->server->get('REQUEST_TIME'),
$user,
$this->request->getClientIp()
);
- $state = AccessResult::forbiddenIf(
+ return $state->andIf(AccessResult::forbiddenIf(
!empty($embargoes),
$this->formatPlural(
count($embargoes),
'1 embargo preventing access.',
'@count embargoes preventing access.'
)->render()
- );
- array_map([$state, 'addCacheableDependency'], $embargoes);
- return $state;
+ ));
+
}
}
diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php
index 81f9c58..c5b0189 100644
--- a/src/Access/QueryTagger.php
+++ b/src/Access/QueryTagger.php
@@ -8,60 +8,21 @@
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
-use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
+use Drupal\embargo\EmbargoExistenceQueryTrait;
use Drupal\embargo\EmbargoInterface;
use Drupal\islandora_hierarchical_access\Access\QueryConjunctionTrait;
-use Drupal\islandora_hierarchical_access\LUTGeneratorInterface;
+use Drupal\islandora_hierarchical_access\TaggedTargetsTrait;
use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Handles tagging entity queries with access restrictions for embargoes.
*/
class QueryTagger {
+ use EmbargoExistenceQueryTrait;
use QueryConjunctionTrait;
-
- /**
- * The current user.
- *
- * @var \Drupal\Core\Session\AccountProxyInterface
- */
- protected $user;
-
- /**
- * The IP of the request.
- *
- * @var string
- */
- protected $currentIp;
-
- /**
- * Instance of a Drupal database connection.
- *
- * @var \Drupal\Core\Database\Connection
- */
- protected $database;
-
- /**
- * The entity type manager.
- *
- * @var \Drupal\Core\Entity\EntityTypeManagerInterface
- */
- protected $entityTypeManager;
-
- /**
- * Time service.
- *
- * @var \Drupal\Component\Datetime\TimeInterface
- */
- protected TimeInterface $time;
-
- /**
- * Date formatter service.
- *
- * @var \Drupal\Core\Datetime\DateFormatterInterface
- */
- protected DateFormatterInterface $dateFormatter;
+ use TaggedTargetsTrait;
/**
* Constructor.
@@ -72,7 +33,8 @@ public function __construct(
Connection $database,
EntityTypeManagerInterface $entity_type_manager,
TimeInterface $time,
- DateFormatterInterface $date_formatter
+ DateFormatterInterface $date_formatter,
+ EventDispatcherInterface $event_dispatcher,
) {
$this->user = $user;
$this->currentIp = $request_stack->getCurrentRequest()->getClientIp();
@@ -80,6 +42,7 @@ public function __construct(
$this->entityTypeManager = $entity_type_manager;
$this->time = $time;
$this->dateFormatter = $date_formatter;
+ $this->setEventDispatcher($event_dispatcher);
}
/**
@@ -87,127 +50,30 @@ public function __construct(
*
* @param \Drupal\Core\Database\Query\SelectInterface $query
* The query being executed.
- * @param string $type
- * Either "node" or "file".
*/
- public function tagAccess(SelectInterface $query, string $type) {
- if (!in_array($type, ['node', 'media', 'file'])) {
- throw new \InvalidArgumentException("Unrecognized type '$type'.");
+ public function tagNode(SelectInterface $query) : void {
+ if ($query->hasTag('islandora_hierarchical_access_subquery')) {
+ // Being run as a subquery: We do not want to touch it as we expect our
+ // IslandoraHierarchicalAccessEventSubscriber class to deal with it.
+ return;
}
- elseif ($this->user->hasPermission('bypass embargo access')) {
+ if ($this->user->hasPermission('bypass embargo access')) {
return;
}
+ $type = 'node';
static::conjunctionQuery($query);
- /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */
- $storage = $this->entityTypeManager->getStorage($type);
- $tables = $storage->getTableMapping()->getTableNames();
+ $tagged_table_aliases = $query->getMetaData('embargo_tagged_table_aliases') ?? [];
- foreach ($query->getTables() as $info) {
- if ($info['table'] instanceof SelectInterface) {
- continue;
- }
- elseif (in_array($info['table'], $tables)) {
- $key = (strpos($info['table'], "{$type}__") === 0) ? 'entity_id' : (substr($type, 0, 1) . "id");
- $alias = $info['alias'];
+ $target_aliases = $this->getTaggingTargets($query, $tagged_table_aliases, $type);
- $to_apply = $query;
- if ($info['join type'] == 'LEFT') {
- $to_apply = $query->orConditionGroup()
- ->isNull("{$alias}.{$key}");
- $query->condition($to_apply);
- }
- if ($type === 'node') {
- $to_apply->condition("{$alias}.{$key}", $this->buildInaccessibleEmbargoesCondition(), 'NOT IN');
- }
- elseif ($type === 'media') {
- $to_apply->condition("{$alias}.{$key}", $this->buildInaccessibleFileCondition('mid'), 'NOT IN');
- }
- elseif ($type === 'file') {
- $to_apply->condition("{$alias}.{$key}", $this->buildInaccessibleFileCondition('fid'), 'NOT IN');
- }
- else {
- throw new \InvalidArgumentException("Invalid type '$type'.");
- }
- }
- }
- }
-
- /**
- * Builds the condition for file-typed embargoes that are inaccessible.
- *
- * @param string $lut_column
- * The particular column of the LUT to return, as file embargoes apply to
- * media ('mid') as well as files ('fid').
- *
- * @return \Drupal\Core\Database\Query\SelectInterface
- * The sub-query to be used that results in all file IDs that cannot be
- * accessed.
- */
- protected function buildInaccessibleFileCondition(string $lut_column) {
- $query = $this->database->select('embargo', 'e');
- $lut_alias = $query->join(LUTGeneratorInterface::TABLE_NAME, 'lut', '%alias.nid = e.embargoed_node');
- return $query
- ->fields($lut_alias, [$lut_column])
- ->condition('lut.nid', $this->buildAccessibleEmbargoesQuery(EmbargoInterface::EMBARGO_TYPE_FILE), 'NOT IN');
- }
-
- /**
- * Get query to select accessible embargoed entities.
- *
- * @param int $type
- * The type of embargo, expected to be one of:
- * - EmbargoInterface::EMBARGO_TYPE_NODE; or,
- * - EmbargoInterface::EMBARGO_TYPE_FILE.
- *
- * @return \Drupal\Core\Database\Query\SelectInterface
- * A query returning things that should not be inaccessible.
- */
- protected function buildAccessibleEmbargoesQuery($type) : SelectInterface {
- $query = $this->database->select('embargo', 'e')
- ->fields('e', ['embargoed_node']);
-
- // Things are visible if...
- $group = $query->orConditionGroup()
- // The selected embargo entity does not apply to the given type; or...
- ->condition('e.embargo_type', $type, '!=');
-
- $group->condition($query->andConditionGroup()
- // ... a scheduled embargo...
- ->condition('e.expiration_type', EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)
- // ... has a date in the past.
- ->condition('e.expiration_date', $this->dateFormatter->format($this->time->getRequestTime(), 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT), '<')
- );
-
- // ... the incoming IP is in an exempt range; or...
- /** @var \Drupal\embargo\IpRangeStorageInterface $storage */
- $storage = $this->entityTypeManager->getStorage('embargo_ip_range');
- $applicable_ip_ranges = $storage->getApplicableIpRanges($this->currentIp);
- if (!empty($applicable_ip_ranges)) {
- $group->condition('e.exempt_ips', array_keys($applicable_ip_ranges), 'IN');
+ if (empty($target_aliases)) {
+ return;
}
- // ... the specific user is exempted from the embargo.
- $user_alias = $query->leftJoin('embargo__exempt_users', 'u', 'e.id = %alias.entity_id');
- $group->condition("{$user_alias}.exempt_users_target_id", $this->user->id());
-
- $query->condition($group);
-
- return $query;
- }
-
- /**
- * Builds the condition for embargoes that are inaccessible.
- *
- * @return \Drupal\Core\Database\Query\SelectInterface
- * The sub-query to be used that results in embargoed_node IDs that
- * cannot be accessed.
- */
- protected function buildInaccessibleEmbargoesCondition() : SelectInterface {
- return $this->database->select('embargo', 'ein')
- ->condition('ein.embargoed_node', $this->buildAccessibleEmbargoesQuery(EmbargoInterface::EMBARGO_TYPE_NODE), 'NOT IN')
- ->fields('ein', ['embargoed_node']);
+ $query->addMetaData('embargo_tagged_table_aliases', $tagged_table_aliases);
+ $this->applyExistenceQuery($query, $target_aliases, [EmbargoInterface::EMBARGO_TYPE_NODE]);
}
}
diff --git a/src/Cache/Context/IpRangeCacheContext.php b/src/Cache/Context/IpRangeCacheContext.php
new file mode 100644
index 0000000..8f77b17
--- /dev/null
+++ b/src/Cache/Context/IpRangeCacheContext.php
@@ -0,0 +1,81 @@
+getRanges());
+ sort($range_keys, SORT_NUMERIC);
+ return implode(',', $range_keys);
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getCacheableMetadata() {
+ $cache_meta = new CacheableMetadata();
+
+ foreach ($this->getRanges() as $range) {
+ $cache_meta->addCacheableDependency($range);
+ }
+
+ return $cache_meta;
+ }
+
+ /**
+ * Get any IP range entities associated with the current IP address.
+ *
+ * @return \Drupal\embargo\IpRangeInterface[]
+ * Any relevant IP range entities.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ protected function getRanges() : array {
+ if (!isset($this->ranges)) {
+ /** @var \Drupal\embargo\IpRangeStorageInterface $embargo_ip_range_storage */
+ $embargo_ip_range_storage = $this->entityTypeManager->getStorage('embargo_ip_range');
+ $this->ranges = $embargo_ip_range_storage->getApplicableIpRanges($this->requestStack->getCurrentRequest()
+ ->getClientIp());
+ }
+
+ return $this->ranges;
+ }
+
+}
diff --git a/src/Cache/Context/UserExemptedCacheContext.php b/src/Cache/Context/UserExemptedCacheContext.php
new file mode 100644
index 0000000..6bcc756
--- /dev/null
+++ b/src/Cache/Context/UserExemptedCacheContext.php
@@ -0,0 +1,77 @@
+isExempted() ? '1' : '0';
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function getCacheableMetadata() {
+ return (new CacheableMetadata())
+ ->addCacheContexts([$this->isExempted() ? 'user' : 'user.permissions'])
+ ->addCacheTags(['embargo_list']);
+ }
+
+ /**
+ * Determine if the current user has _any_ exemptions.
+ *
+ * @return bool
+ * TRUE if the user is exempt to at least one embargo; otherwise, FALSE.
+ *
+ * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
+ * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ */
+ protected function isExempted() : bool {
+ if (!isset($this->exempted)) {
+ $results = $this->entityTypeManager->getStorage('embargo')->getQuery()
+ ->accessCheck(FALSE)
+ ->condition('exempt_users', $this->currentUser->id())
+ ->range(0, 1)
+ ->execute();
+ $this->exempted = !empty($results);
+ }
+
+ return $this->exempted;
+ }
+
+}
diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php
new file mode 100644
index 0000000..18db51b
--- /dev/null
+++ b/src/EmbargoExistenceQueryTrait.php
@@ -0,0 +1,247 @@
+condition(
+ $existence_condition->orConditionGroup()
+ ->notExists($this->getNullQuery($target_aliases, $embargo_types))
+ ->exists($this->getAccessibleEmbargoesQuery($target_aliases, $embargo_types))
+ );
+ }
+
+ /**
+ * Set the event dispatcher service.
+ *
+ * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
+ * The event dispatcher service to set.
+ *
+ * @return \Drupal\embargo\EmbargoExistenceQueryTrait|\Drupal\embargo\Access\QueryTagger|\Drupal\embargo\EventSubscriber\IslandoraHierarchicalAccessEventSubscriber
+ * The current instance; fluent interface.
+ */
+ protected function setEventDispatcher(EventDispatcherInterface $event_dispatcher) : self {
+ $this->eventDispatcher = $event_dispatcher;
+ return $this;
+ }
+
+ /**
+ * Get the event dispatcher service.
+ *
+ * @return \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
+ * The event dispatcher service.
+ */
+ protected function getEventDispatch() : EventDispatcherInterface {
+ return $this->eventDispatcher ?? \Drupal::service('event_dispatcher');
+ }
+
+ /**
+ * Build out condition for matching embargo entities.
+ *
+ * @param \Drupal\Core\Database\Query\SelectInterface $query
+ * The query in which the condition is to be attached.
+ *
+ * @return \Drupal\Core\Database\Query\ConditionInterface
+ * The condition to attach.
+ */
+ protected function buildInclusionBaseCondition(SelectInterface $query) : ConditionInterface {
+ $dispatched_event = $this->getEventDispatch()->dispatch(new TagInclusionEvent($query));
+
+ return $dispatched_event->getCondition();
+ }
+
+ /**
+ * Build out condition for matching overriding embargo entities.
+ *
+ * @param \Drupal\Core\Database\Query\SelectInterface $query
+ * The query in which the condition is to be attached.
+ *
+ * @return \Drupal\Core\Database\Query\ConditionInterface
+ * The condition to attach.
+ */
+ protected function buildExclusionBaseCondition(SelectInterface $query) : ConditionInterface {
+ $dispatched_event = $this->getEventDispatch()->dispatch(new TagExclusionEvent($query));
+
+ return $dispatched_event->getCondition();
+ }
+
+ /**
+ * Get query for negative assertion.
+ *
+ * @param array $target_aliases
+ * The target aliases on which to match.
+ * @param array $embargo_types
+ * The relevant types of embargoes to which to constrain.
+ *
+ * @return \Drupal\Core\Database\Query\SelectInterface
+ * The negative-asserting query.
+ */
+ protected function getNullQuery(array $target_aliases, array $embargo_types) : SelectInterface {
+ $embargo_alias = 'embargo_null';
+ $query = $this->database->select('embargo', $embargo_alias);
+ $query->addExpression(1, 'embargo_null_e');
+ $query->addMetaData('embargo_alias', $embargo_alias);
+ $query->addMetaData('embargo_target_aliases', $target_aliases);
+
+ $query->condition($this->buildInclusionBaseCondition($query));
+ $query->condition("{$embargo_alias}.embargo_type", $embargo_types, 'IN');
+
+ return $query;
+ }
+
+ /**
+ * Get query for positive assertion.
+ *
+ * @param array $target_aliases
+ * The target aliases on which to match.
+ * @param array $embargo_types
+ * The relevant types of embargoes to which to constrain.
+ *
+ * @return \Drupal\Core\Database\Query\SelectInterface
+ * The positive-asserting query.
+ */
+ protected function getAccessibleEmbargoesQuery(array $target_aliases, array $embargo_types) : SelectInterface {
+ // Embargo exists for the entity, where:
+ $embargo_alias = 'embargo_existence';
+ $embargo_existence = $this->database->select('embargo', $embargo_alias);
+ $embargo_existence->addExpression(1, 'embargo_allowed');
+
+ $embargo_existence->addMetaData('embargo_alias', $embargo_alias);
+ $embargo_existence->addMetaData('embargo_target_aliases', $target_aliases);
+
+ $embargo_existence->condition(
+ $embargo_existence->orConditionGroup()
+ ->condition($existence_condition = $embargo_existence->andConditionGroup()
+ ->condition($this->buildInclusionBaseCondition($embargo_existence))
+ ->condition($embargo_or = $embargo_existence->orConditionGroup())
+ )
+ );
+
+ $embargo_existence->addMetaData('embargo_existence_condition', $existence_condition);
+
+ // - The request IP is exempt.
+ /** @var \Drupal\embargo\IpRangeStorageInterface $storage */
+ $storage = $this->entityTypeManager->getStorage('embargo_ip_range');
+ $applicable_ip_ranges = $storage->getApplicableIpRanges($this->currentIp);
+ if ($applicable_ip_ranges) {
+ $embargo_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN');
+ }
+
+ // - The user is exempt.
+ // @todo Should the IP range constraint(s) take precedence?
+ $user_existence = $this->database->select('embargo__exempt_users', 'eeu');
+ $user_existence->addExpression(1, 'user_existence');
+ $user_existence->where("eeu.entity_id = {$embargo_alias}.id")
+ ->condition('eeu.exempt_users_target_id', $this->user->id());
+ $embargo_or->exists($user_existence);
+
+ // - There's a scheduled embargo of an appropriate type and no other
+ // overriding embargo.
+ $current_date = $this->dateFormatter->format($this->time->getRequestTime(), 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT);
+ // No indefinite embargoes or embargoes expiring in the future.
+ $unexpired_embargo_subquery = $this->database->select('embargo', 'ue')
+ ->addMetaData('embargo_alias', $embargo_alias)
+ ->addMetaData('embargo_target_aliases', $target_aliases)
+ ->addMetaData('embargo_unexpired_alias', 'ue');
+ $unexpired_embargo_subquery->condition($this->buildExclusionBaseCondition($unexpired_embargo_subquery))
+ ->condition('ue.embargo_type', $embargo_types, 'IN');
+ $unexpired_embargo_subquery->addExpression(1, 'ueee');
+ $unexpired_embargo_subquery->condition($unexpired_embargo_subquery->orConditionGroup()
+ ->condition('ue.expiration_type', EmbargoInterface::EXPIRATION_TYPE_INDEFINITE)
+ ->condition($unexpired_embargo_subquery->andConditionGroup()
+ ->condition('ue.expiration_type', EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)
+ ->condition('ue.expiration_date', $current_date, '>')
+ )
+ );
+
+ $embargo_or->condition(
+ $embargo_or->andConditionGroup()
+ ->condition("{$embargo_alias}.embargo_type", $embargo_types, 'IN')
+ ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)
+ ->condition("{$embargo_alias}.expiration_date", $current_date, '<=')
+ ->notExists($unexpired_embargo_subquery)
+ );
+
+ return $embargo_existence;
+ }
+
+}
diff --git a/src/EmbargoIpRangeViewsData.php b/src/EmbargoIpRangeViewsData.php
new file mode 100644
index 0000000..0816412
--- /dev/null
+++ b/src/EmbargoIpRangeViewsData.php
@@ -0,0 +1,12 @@
+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/EmbargoStorage.php b/src/EmbargoStorage.php
index 9221119..5f8f0e9 100644
--- a/src/EmbargoStorage.php
+++ b/src/EmbargoStorage.php
@@ -2,116 +2,24 @@
namespace Drupal\embargo;
-use Drupal\Core\Cache\CacheBackendInterface;
-use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
-use Drupal\Core\Database\Connection;
-use Drupal\Core\Entity\EntityFieldManagerInterface;
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
-use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
-use Drupal\Core\Language\LanguageManagerInterface;
-use Drupal\Core\Session\AccountInterface;
-use Drupal\file\FileInterface;
-use Drupal\islandora_hierarchical_access\LUTGeneratorInterface;
-use Drupal\media\MediaInterface;
-use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\RequestStack;
/**
* Storage for embargo entities.
*/
class EmbargoStorage extends SqlContentEntityStorage implements EmbargoStorageInterface {
- /**
- * The current request.
- *
- * @var \Symfony\Component\HttpFoundation\Request
- */
- protected $request;
-
- /**
- * The current user.
- *
- * @var \Drupal\Core\Session\AccountInterface
- */
- protected $user;
-
- /**
- * Constructor.
- */
- public function __construct(EntityTypeInterface $entity_type, Connection $database, EntityFieldManagerInterface $entity_field_manager, CacheBackendInterface $cache, LanguageManagerInterface $language_manager, MemoryCacheInterface $memory_cache, EntityTypeBundleInfoInterface $entity_type_bundle_info, EntityTypeManagerInterface $entity_type_manager, RequestStack $request_stack, AccountInterface $user) {
- parent::__construct($entity_type, $database, $entity_field_manager, $cache, $language_manager, $memory_cache, $entity_type_bundle_info, $entity_type_manager);
- $this->request = $request_stack->getCurrentRequest();
- $this->user = $user;
- }
+ use EmbargoStorageTrait;
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
- return new static(
- $entity_type,
- $container->get('database'),
- $container->get('entity_field.manager'),
- $container->get('cache.entity'),
- $container->get('language_manager'),
- $container->get('entity.memory_cache'),
- $container->get('entity_type.bundle.info'),
- $container->get('entity_type.manager'),
- $container->get('request_stack'),
- $container->get('current_user'),
- );
- }
-
- /**
- * {@inheritdoc}
- */
- public static function applicableEntityTypes(): array {
- return [
- 'node',
- 'media',
- 'file',
- ];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getApplicableEmbargoes(EntityInterface $entity): array {
- if ($entity instanceof NodeInterface) {
- $properties = ['embargoed_node' => $entity->id()];
- return $this->loadByProperties($properties);
- }
- elseif ($entity instanceof MediaInterface || $entity instanceof FileInterface) {
- $query = $this->database->select('embargo', 'e')
- ->fields('e', ['id'])
- ->distinct();
- $lut_alias = $query->join(LUTGeneratorInterface::TABLE_NAME, 'lut', '%alias.nid = e.embargoed_node');
- $key = $entity instanceof MediaInterface ? 'mid' : 'fid';
- $query->condition("{$lut_alias}.{$key}", $entity->id());
- $ids = $query->execute()->fetchCol();
- return $this->loadMultiple($ids);
- }
- return [];
- }
-
- /**
- * {@inheritdoc}
- */
- public function getApplicableNonExemptNonExpiredEmbargoes(EntityInterface $entity, ?int $timestamp = NULL, ?AccountInterface $user = NULL, ?string $ip = NULL): array {
- $timestamp = $timestamp ?? $this->request->server->get('REQUEST_TIME');
- $user = $user ?? $this->user;
- $ip = $ip ?? $this->request->getClientIp();
- return array_filter($this->getApplicableEmbargoes($entity), function ($embargo) use ($entity, $timestamp, $user, $ip): bool {
- $inactive = $embargo->expiresBefore($timestamp);
- $type_exempt = ($entity instanceof NodeInterface && $embargo->getEmbargoType() !== EmbargoInterface::EMBARGO_TYPE_NODE);
- $user_exempt = $embargo->isUserExempt($user);
- $ip_exempt = $embargo->ipIsExempt($ip);
- return !($inactive || $type_exempt || $user_exempt || $ip_exempt);
- });
+ return parent::createInstance($container, $entity_type)
+ ->setRequest($container->get('request_stack')->getCurrentRequest())
+ ->setUser($container->get('current_user'));
}
}
diff --git a/src/EmbargoStorageInterface.php b/src/EmbargoStorageInterface.php
index 67aa737..af1cffc 100644
--- a/src/EmbargoStorageInterface.php
+++ b/src/EmbargoStorageInterface.php
@@ -11,11 +11,19 @@
*/
interface EmbargoStorageInterface extends ContentEntityStorageInterface {
+ const APPLICABLE_ENTITY_TYPES = [
+ 'node',
+ 'media',
+ 'file',
+ ];
+
/**
* A list of entity types which an embargo can apply to.
*
* @return string[]
* A list of entity types identifiers which an embargo can apply to.
+ *
+ * @obsolete
*/
public static function applicableEntityTypes();
diff --git a/src/EmbargoStorageTrait.php b/src/EmbargoStorageTrait.php
new file mode 100644
index 0000000..ef640ff
--- /dev/null
+++ b/src/EmbargoStorageTrait.php
@@ -0,0 +1,109 @@
+ $entity->id()];
+ return $this->loadByProperties($properties);
+ }
+ elseif ($entity instanceof MediaInterface || $entity instanceof FileInterface) {
+ $query = $this->database->select('embargo', 'e')
+ ->fields('e', ['id'])
+ ->distinct();
+ $lut_alias = $query->join(LUTGeneratorInterface::TABLE_NAME, 'lut', '%alias.nid = e.embargoed_node');
+ $key = $entity instanceof MediaInterface ? 'mid' : 'fid';
+ $query->condition("{$lut_alias}.{$key}", $entity->id());
+ $ids = $query->execute()->fetchCol();
+ return $this->loadMultiple($ids);
+ }
+ return [];
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getApplicableNonExemptNonExpiredEmbargoes(EntityInterface $entity, ?int $timestamp = NULL, ?AccountInterface $user = NULL, ?string $ip = NULL): array {
+ $timestamp = $timestamp ?? $this->request->server->get('REQUEST_TIME');
+ $user = $user ?? $this->user;
+ $ip = $ip ?? $this->request->getClientIp();
+ return array_filter($this->getApplicableEmbargoes($entity), function ($embargo) use ($entity, $timestamp, $user, $ip): bool {
+ $inactive = $embargo->expiresBefore($timestamp);
+ $type_exempt = ($entity instanceof NodeInterface && $embargo->getEmbargoType() !== EmbargoInterface::EMBARGO_TYPE_NODE);
+ $user_exempt = $embargo->isUserExempt($user);
+ $ip_exempt = $embargo->ipIsExempt($ip);
+ return !($inactive || $type_exempt || $user_exempt || $ip_exempt);
+ });
+ }
+
+ /**
+ * Set the user visible to the trait.
+ *
+ * @param \Drupal\Core\Session\AccountInterface $user
+ * The user with which to evaluate.
+ *
+ * @return \Drupal\embargo\EmbargoStorageInterface|\Drupal\embargo\EmbargoStorageTrait
+ * Fluent interface; the current object.
+ */
+ protected function setUser(AccountInterface $user) : self {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * The request visible to the trait.
+ *
+ * @param \Symfony\Component\HttpFoundation\Request|null $request
+ * The request with which to evaluate.
+ *
+ * @return \Drupal\embargo\EmbargoStorageInterface|\Drupal\embargo\EmbargoStorageTrait
+ * Fluent interface; the current object.
+ */
+ protected function setRequest(?Request $request) : self {
+ $this->request = $request;
+ return $this;
+ }
+
+}
diff --git a/src/EmbargoViewsData.php b/src/EmbargoViewsData.php
new file mode 100644
index 0000000..380a83d
--- /dev/null
+++ b/src/EmbargoViewsData.php
@@ -0,0 +1,12 @@
+getExpirationType() === static::EXPIRATION_TYPE_SCHEDULED && !$this->expiresBefore($now)) {
- return $this->getExpirationDate()->getTimestamp() - $now;
+ $max_age = Cache::mergeMaxAges($max_age, $this->getExpirationDate()->getTimestamp() - $now);
}
- // Other properties of the embargo are not time dependent.
- return parent::getCacheMaxAge();
+
+ return $max_age;
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
- $tags = parent::getCacheTags();
- $tags[] = "node:{$this->getEmbargoedNode()->id()}";
+ $tags = Cache::mergeTags(parent::getCacheTags(), $this->getEmbargoedNode()->getCacheTags());
+
if ($this->getExemptIps()) {
$tags = Cache::mergeTags($tags, $this->getExemptIps()->getCacheTags());
}
return $tags;
}
+ /**
+ * {@inheritDoc}
+ */
+ public function getCacheContexts() {
+ $contexts = Cache::mergeContexts(
+ parent::getCacheContexts(),
+ $this->getEmbargoedNode()->getCacheContexts(),
+ ['user.embargo__has_exemption'],
+ );
+
+ if ($this->getExemptIps()) {
+ $contexts = Cache::mergeContexts($contexts, $this->getExemptIps()->getCacheContexts());
+ }
+
+ return $contexts;
+ }
+
/**
* {@inheritdoc}
*/
@@ -434,4 +451,16 @@ public function ipIsExempt(string $ip): bool {
return $exempt_ips && $exempt_ips->withinRanges($ip);
}
+ /**
+ * {@inheritdoc}
+ */
+ protected function getListCacheTagsToInvalidate() : array {
+ return array_merge(
+ parent::getListCacheTagsToInvalidate(),
+ array_map(function (string $type) {
+ return "{$type}_list";
+ }, EmbargoStorageInterface::APPLICABLE_ENTITY_TYPES),
+ );
+ }
+
}
diff --git a/src/Entity/IpRange.php b/src/Entity/IpRange.php
index 60bea11..1794c53 100644
--- a/src/Entity/IpRange.php
+++ b/src/Entity/IpRange.php
@@ -3,12 +3,14 @@
namespace Drupal\embargo\Entity;
use Drupal\Component\Utility\UrlHelper;
+use Drupal\Core\Cache\Cache;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\embargo\EmbargoStorageInterface;
use Drupal\embargo\IpRangeInterface;
use Symfony\Component\HttpFoundation\IpUtils;
@@ -28,6 +30,7 @@
* handlers = {
* "storage" = "Drupal\embargo\IpRangeStorage",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
+ * "views_data" = "Drupal\embargo\EmbargoIpRangeViewsData",
* "list_builder" = "Drupal\embargo\EmbargoListBuilder",
* "form" = {
* "add" = "Drupal\embargo\Form\IpRangeForm",
@@ -38,7 +41,6 @@
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider"
* },
* },
- * list_cache_tags = { "node_list", "media_list", "file_list" },
* base_table = "embargo_ip_range",
* admin_permission = "administer embargo",
* entity_keys = {
@@ -239,4 +241,25 @@ public static function isValidCidr(string $cidr): bool {
return FALSE;
}
+ /**
+ * {@inheritDoc}
+ */
+ public function getCacheContexts() {
+ return Cache::mergeContexts(parent::getCacheContexts(), [
+ 'ip.embargo_range',
+ ]);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function getListCacheTagsToInvalidate() : array {
+ return array_merge(
+ parent::getListCacheTagsToInvalidate(),
+ array_map(function (string $type) {
+ return "{$type}_list";
+ }, EmbargoStorageInterface::APPLICABLE_ENTITY_TYPES),
+ );
+ }
+
}
diff --git a/src/Event/AbstractTagEvent.php b/src/Event/AbstractTagEvent.php
new file mode 100644
index 0000000..220c62c
--- /dev/null
+++ b/src/Event/AbstractTagEvent.php
@@ -0,0 +1,70 @@
+condition = $this->query->orConditionGroup();
+ }
+
+ /**
+ * Get the query upon which to act.
+ *
+ * @return \Drupal\Core\Database\Query\SelectInterface
+ * The query upon which we are to act.
+ */
+ public function getQuery() : SelectInterface {
+ return $this->query;
+ }
+
+ /**
+ * Get the current condition.
+ *
+ * @return \Drupal\Core\Database\Query\ConditionInterface
+ * The current condition.
+ */
+ public function getCondition() : ConditionInterface {
+ return $this->condition;
+ }
+
+ /**
+ * Get the base "embargo" table alias.
+ *
+ * @return string
+ * The base "embargo" alias, as used in the query.
+ */
+ public function getEmbargoAlias() : string {
+ return $this->query->getMetaData('embargo_alias');
+ }
+
+ /**
+ * Get the base query columns representing node IDs to find embargoes.
+ *
+ * @return string[]
+ * The column aliases representing node IDs.
+ */
+ public function getTargetAliases() : array {
+ return $this->query->getMetaData('embargo_target_aliases');
+ }
+
+}
diff --git a/src/Event/EmbargoEvents.php b/src/Event/EmbargoEvents.php
new file mode 100644
index 0000000..effceb4
--- /dev/null
+++ b/src/Event/EmbargoEvents.php
@@ -0,0 +1,14 @@
+query->getMetaData('embargo_unexpired_alias');
+ }
+
+}
diff --git a/src/Event/TagInclusionEvent.php b/src/Event/TagInclusionEvent.php
new file mode 100644
index 0000000..42937a7
--- /dev/null
+++ b/src/Event/TagInclusionEvent.php
@@ -0,0 +1,10 @@
+get('search_api.fields_helper'),
+ $container->get('current_user'),
+ $container->get('entity_type.manager'),
+ $container->get('request_stack'),
+ );
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function getSubscribedEvents() {
+ $events = [];
+
+ if (class_exists(SearchApiSolrEvents::class)) {
+ $events += [
+ SearchApiSolrEvents::PRE_QUERY => 'preQuery',
+ ];
+ }
+
+ return $events;
+ }
+
+ /**
+ * Event handler; respond to search_api_solr pre-query event.
+ *
+ * @param \Drupal\search_api_solr\Event\PreQueryEvent $event
+ * The event to which to respond.
+ */
+ public function preQuery(PreQueryEvent $event) : void {
+ $search_api_query = $event->getSearchApiQuery();
+ if (!$search_api_query->hasTag('embargo_join_processor')) {
+ return;
+ }
+
+ $queries = $search_api_query->getOption('embargo_join_processor__queries', []);
+
+ if (!$queries) {
+ return;
+ }
+
+ $backend = $search_api_query->getIndex()->getServerInstance()->getBackend();
+ assert($backend instanceof SolrBackendInterface);
+ $map = $backend->getSolrFieldNames($search_api_query->getIndex());
+ $memoized_map = [];
+ $get_field_name = function (?string $datasource_id, string $property_path) use ($search_api_query, $map, &$memoized_map) {
+ $key = "{$datasource_id}__{$property_path}";
+ if (!isset($memoized_map[$key])) {
+ $fields = $this->fieldsHelper->filterForPropertyPath(
+ $search_api_query->getIndex()->getFieldsByDatasource($datasource_id),
+ $datasource_id,
+ $property_path,
+ );
+ /** @var \Drupal\search_api\Item\FieldInterface $field */
+ $field = reset($fields);
+
+ $memoized_map[$key] = $map[$field->getFieldIdentifier()];
+ }
+
+ return $memoized_map[$key];
+ };
+
+ $solarium_query = $event->getSolariumQuery();
+ assert($solarium_query instanceof SolariumSelectQuery);
+ $helper = $solarium_query->getHelper();
+
+ /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */
+ $ip_range_entities = $search_api_query->getOption('embargo_join_processor__ip_ranges', []);
+
+ foreach ($queries as $type => $info) {
+ $solarium_query->createFilterQuery([
+ 'key' => "embargo_join:{$type}",
+ 'query' => strtr(
+ implode(' ', [
+ '(*:* -!datasource_field:(!datasources))',
+ '(*:* -_query_:"!join*:*")',
+ '_query_:"!join(',
+ implode(' ', [
+ '+(*:* -!type_field:\\"0\\")',
+ '+!type_field:\\"1\\"',
+ '+!date_field:[* TO \\"!date_value\\"]',
+ '+(*:* -!date_field:[\\"!next_date_value\\" TO *])',
+ ]),
+ ')"',
+ '!join!exempt_user_field:"!current_user"',
+ $ip_range_entities ? '_query_:"!join!exempt_ip_field:(!exempt_ip_ranges)"' : '',
+ ]),
+ [
+ '!join' => $helper->join(
+ $get_field_name(NULL, $info['embargo path']),
+ $get_field_name(NULL, $info['node path']),
+ ),
+ '!type_field' => $get_field_name('entity:embargo', 'expiration_type'),
+ '!exempt_user_field' => $get_field_name('entity:embargo', 'exempt_users:entity:uid'),
+ '!current_user' => $this->currentUser->id(),
+ '!exempt_ip_field' => $get_field_name('entity:embargo', 'exempt_ips:entity:id'),
+ '!exempt_ip_ranges' => implode(
+ ' ',
+ array_map(
+ $helper->escapeTerm(...),
+ array_map(
+ function ($range) {
+ return $range->id();
+ },
+ $ip_range_entities
+ )
+ )
+ ),
+ '!embargo_id' => $get_field_name('entity:embargo', 'id'),
+ '!date_field' => $get_field_name('entity:embargo', 'expiration_date'),
+ '!date_value' => $helper->formatDate(strtotime('now')),
+ '!next_date_value' => $helper->formatDate(strtotime('now + 1day')),
+ '!datasource_field' => $map['search_api_datasource'],
+ '!datasources' => implode(',', array_map(
+ function (string $source_id) {
+ return strtr('"!source"', [
+ '!source' => $source_id,
+ ]);
+ },
+ $info['data sources'],
+ )),
+ ],
+ ),
+ ])->addTag("embargo_join_processor:{$type}");
+ }
+
+ }
+
+}
diff --git a/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php
new file mode 100644
index 0000000..56010e3
--- /dev/null
+++ b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php
@@ -0,0 +1,98 @@
+currentIp = $this->requestStack->getCurrentRequest()->getClientIp();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function create(ContainerInterface $container) : self {
+ return (new static(
+ $container->get('current_user'),
+ $container->get('request_stack'),
+ $container->get('database'),
+ $container->get('entity_type.manager'),
+ $container->get('datetime.time'),
+ $container->get('date.formatter'),
+ ))
+ ->setEventDispatcher($container->get('event_dispatcher'));
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function getSubscribedEvents() : array {
+ return [
+ Event::class => 'processEvent',
+ ];
+ }
+
+ /**
+ * Process the islandora_hierarchical_access query alter event.
+ *
+ * @param \Drupal\islandora_hierarchical_access\Event\Event $event
+ * The event to process.
+ */
+ public function processEvent(Event $event) : void {
+ $query = $event->getQuery();
+ if ($event->getQuery()->hasTag(static::TAG)) {
+ return;
+ }
+
+ $query->addTag(static::TAG);
+
+ if ($this->user->hasPermission('bypass embargo access')) {
+ return;
+ }
+
+ /** @var \Drupal\Core\Database\Query\ConditionInterface $existence_condition */
+ $existence_condition = $query->getMetaData('islandora_hierarchical_access_tagged_existence_condition');
+ $this->applyExistenceQuery(
+ $existence_condition,
+ ['lut_exist.nid'],
+ match ($event->getType()) {
+ 'file', 'media' => [
+ EmbargoInterface::EMBARGO_TYPE_FILE,
+ EmbargoInterface::EMBARGO_TYPE_NODE,
+ ],
+ 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE],
+ },
+ );
+ }
+
+}
diff --git a/src/EventSubscriber/TaggingEventSubscriber.php b/src/EventSubscriber/TaggingEventSubscriber.php
new file mode 100644
index 0000000..0044ca5
--- /dev/null
+++ b/src/EventSubscriber/TaggingEventSubscriber.php
@@ -0,0 +1,51 @@
+ 'inclusion',
+ EmbargoEvents::TAG_EXCLUSION => 'exclusion',
+ ];
+ }
+
+ /**
+ * Event handler; tagging inclusion event.
+ *
+ * @param \Drupal\embargo\Event\TagInclusionEvent $event
+ * The event being handled.
+ */
+ public function inclusion(TagInclusionEvent $event) : void {
+ $event->getCondition()->where(strtr('!field IN (!targets)', [
+ '!field' => "{$event->getEmbargoAlias()}.embargoed_node",
+ '!targets' => implode(', ', $event->getTargetAliases()),
+ ]));
+ }
+
+ /**
+ * Event handler; tagging exclusion event.
+ *
+ * @param \Drupal\embargo\Event\TagExclusionEvent $event
+ * The event being handled.
+ */
+ public function exclusion(TagExclusionEvent $event) : void {
+ // With traversing a single level of the hierarchy, it makes sense to
+ // constrain to the same node as matched in the "inclusion", instead of
+ // again referencing the other aliased columns dealing with node IDs.
+ $event->getCondition()->where("{$event->getUnexpiredAlias()}.embargoed_node = {$event->getEmbargoAlias()}.embargoed_node");
+ }
+
+}
diff --git a/src/Plugin/Block/EmbargoNotificationBlock.php b/src/Plugin/Block/EmbargoNotificationBlock.php
index 7ad8d4d..10e5eb1 100644
--- a/src/Plugin/Block/EmbargoNotificationBlock.php
+++ b/src/Plugin/Block/EmbargoNotificationBlock.php
@@ -5,16 +5,15 @@
use Drupal\Component\Utility\Html;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Cache\Cache;
-use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\ResettableStackedRouteMatchInterface;
-use Drupal\Core\Session\AccountProxyInterface;
+use Drupal\Core\Session\AccountInterface;
use Drupal\embargo\EmbargoInterface;
use Drupal\node\NodeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
-use Symfony\Component\HttpFoundation\RequestStack;
+use Symfony\Component\HttpFoundation\Request;
/**
* Provides a "Embargo Notifications" block.
@@ -30,116 +29,81 @@ class EmbargoNotificationBlock extends BlockBase implements ContainerFactoryPlug
/**
* The admin email address.
*
- * @var string
+ * @var string|null
*/
- protected $adminMail;
+ protected ?string $adminMail;
/**
* The notification message.
*
* @var string
*/
- protected $notificationMessage;
+ protected string $notificationMessage;
/**
* A route matching interface.
*
* @var \Drupal\Core\Routing\ResettableStackedRouteMatchInterface
*/
- protected $routeMatch;
+ protected ResettableStackedRouteMatchInterface $routeMatch;
/**
* The request object.
*
* @var \Symfony\Component\HttpFoundation\Request
*/
- protected $request;
-
- /**
- * Embargo entity storage.
- *
- * @var \Drupal\embargo\EmbargoStorageInterface
- */
- protected $storage;
+ protected Request $request;
/**
* The current user.
*
- * @var \Drupal\Core\Session\AccountProxyInterface
+ * @var \Drupal\Core\Session\AccountInterface
*/
- protected $user;
+ protected AccountInterface $user;
/**
* The object renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
- protected $renderer;
+ protected RendererInterface $renderer;
/**
- * Construct embargo notification block.
- *
- * @param array $configuration
- * Block configuration.
- * @param string $plugin_id
- * The plugin ID.
- * @param mixed $plugin_definition
- * The plugin definition.
- * @param \Drupal\Core\Routing\ResettableStackedRouteMatchInterface $route_match
- * A route matching interface.
- * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
- * The request being made to check access against.
- * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
- * A configuration factory interface.
- * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
- * An entity type manager.
- * @param \Drupal\Core\Session\AccountProxyInterface $user
- * The current user.
- * @param \Drupal\Core\Render\RendererInterface $renderer
- * The object renderer.
+ * Drupal's entity type manager service.
*
- * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
- * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
- public function __construct(array $configuration, $plugin_id, $plugin_definition, ResettableStackedRouteMatchInterface $route_match, RequestStack $request_stack, ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, AccountProxyInterface $user, RendererInterface $renderer) {
- parent::__construct($configuration, $plugin_id, $plugin_definition);
- $settings = $config_factory->get('embargo.settings');
- $this->adminMail = $settings->get('contact_email');
- $this->notificationMessage = $settings->get('notification_message');
- $this->storage = $entity_type_manager->getStorage('embargo');
- $this->routeMatch = $route_match;
- $this->request = $request_stack->getCurrentRequest();
- $this->user = $user;
- $this->renderer = $renderer;
- }
+ protected EntityTypeManagerInterface $entityTypeManager;
/**
* {@inheritdoc}
*/
- public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
- return new static(
- $configuration,
- $plugin_id,
- $plugin_definition,
- $container->get('current_route_match'),
- $container->get('request_stack'),
- $container->get('config.factory'),
- $container->get('entity_type.manager'),
- $container->get('current_user'),
- $container->get('renderer'),
- );
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) : self {
+ $instance = new static($configuration, $plugin_id, $plugin_definition);
+
+ $settings = $container->get('config.factory')->get('embargo.settings');
+ $instance->adminMail = $settings->get('contact_email');
+ $instance->notificationMessage = $settings->get('notification_message');
+ $instance->entityTypeManager = $container->get('entity_type.manager');
+ $instance->routeMatch = $container->get('current_route_match');
+ $instance->request = $container->get('request_stack')->getCurrentRequest();
+ $instance->user = $container->get('current_user');
+ $instance->renderer = $container->get('renderer');
+
+ return $instance;
}
/**
* {@inheritdoc}
*/
- public function build() {
- $node = $this->routeMatch->getParameter('node');
- if (!($node instanceof NodeInterface)) {
+ public function build() : array {
+ if (!($node = $this->getNode())) {
return [];
}
// Displays even if the embargo is exempt in the current context.
- $applicable_embargoes = $this->storage->getApplicableEmbargoes($node);
+ /** @var \Drupal\embargo\EmbargoStorageInterface $storage */
+ $storage = $this->entityTypeManager->getStorage('embargo');
+ $applicable_embargoes = $storage->getApplicableEmbargoes($node);
if (empty($applicable_embargoes)) {
return [];
}
@@ -151,7 +115,9 @@ public function build() {
$expired = $embargo->expiresBefore($now);
$exempt_user = $embargo->isUserExempt($this->user);
$exempt_ip = $embargo->ipIsExempt($ip);
+
$embargoes[$id] = [
+ 'actual' => $embargo,
'indefinite' => $embargo->getExpirationType() === EmbargoInterface::EXPIRATION_TYPE_INDEFINITE,
'expired' => $expired,
'exempt_user' => $exempt_user,
@@ -169,6 +135,7 @@ public function build() {
'additional_emails' => $embargo->additional_emails->view('default'),
];
}
+
$build = [
'#theme' => 'embargo_notification',
'#message' => $this->t($this->notificationMessage, ['@contact' => $this->adminMail]), // phpcs:ignore
@@ -184,25 +151,54 @@ public function build() {
/**
* {@inheritdoc}
*/
- public function getCacheTags() {
+ public function getCacheTags() : array {
+ $tags = parent::getCacheTags();
+
// When the given node changes (route), the block should rebuild.
- if ($node = $this->routeMatch->getParameter('node')) {
- return Cache::mergeTags(
- parent::getCacheTags(),
+ if ($node = $this->getNode()) {
+ $tags = Cache::mergeTags(
+ $tags,
$node->getCacheTags(),
);
}
- // Return default tags, if not on a node page.
- return parent::getCacheTags();
+ return $tags;
}
/**
* {@inheritdoc}
*/
- public function getCacheContexts() {
- // Ensure that with every new node/route, this block will be rebuilt.
- return Cache::mergeContexts(parent::getCacheContexts(), ['route']);
+ public function getCacheContexts() : array {
+ $contexts = Cache::mergeContexts(
+ parent::getCacheContexts(),
+ // Ensure that with every new node/route, this block will be rebuilt.
+ [
+ 'route',
+ 'url',
+ ],
+ );
+
+ if ($node = $this->getNode()) {
+ $contexts = Cache::mergeContexts(
+ $contexts,
+ $node->getCacheContexts(),
+ );
+ }
+
+ return $contexts;
+ }
+
+ /**
+ * Helper; get the active node.
+ *
+ * @return \Drupal\node\NodeInterface|null
+ * Get the active node.
+ */
+ protected function getNode() : ?NodeInterface {
+ $node_candidate = $this->routeMatch->getParameter('node');
+ return $node_candidate instanceof NodeInterface ?
+ $node_candidate :
+ NULL;
}
}
diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php
new file mode 100644
index 0000000..90f4cf0
--- /dev/null
+++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php
@@ -0,0 +1,309 @@
+currentUser = $container->get('current_user');
+ $instance->database = $container->get('database');
+ $instance->entityTypeManager = $container->get('entity_type.manager');
+ $instance->requestStack = $container->get('request_stack');
+
+ return $instance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public static function supportsIndex(IndexInterface $index) {
+ return parent::supportsIndex($index) &&
+ in_array('entity:embargo', $index->getDatasourceIds()) &&
+ array_intersect(
+ $index->getDatasourceIds(),
+ array_map(function (string $type) {
+ return "entity:{$type}";
+ }, static::ENTITY_TYPES)
+ );
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array {
+ $properties = [];
+
+ if ($datasource === NULL) {
+ // Represent the node(s) to which a general content entity is associated.
+ $properties[static::NODE_FIELD] = new ProcessorProperty([
+ 'processor_id' => $this->getPluginId(),
+ 'is_list' => TRUE,
+ 'is_computed' => TRUE,
+ ]);
+ // Represent the node of which a "file" embargo is associated.
+ $properties[static::EMBARGO_FIELD_FILE] = new ProcessorProperty([
+ 'processor_id' => $this->getPluginId(),
+ 'is_list' => FALSE,
+ 'is_computed' => TRUE,
+ ]);
+ // Represent the node of which a "node" embargo is associated.
+ $properties[static::EMBARGO_FIELD_NODE] = new ProcessorProperty([
+ 'processor_id' => $this->getPluginId(),
+ 'is_list' => FALSE,
+ 'is_computed' => TRUE,
+ ]);
+ }
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Adapted from search_api's reverse_entity_references processor.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\processor\ReverseEntityReferences::addFieldValues()
+ */
+ public function addFieldValues(ItemInterface $item) : void {
+ if (!in_array($item->getDatasource()->getEntityTypeId(), static::ALL_ENTITY_TYPES)) {
+ return;
+ }
+ try {
+ $entity = $item->getOriginalObject()->getValue();
+ }
+ catch (SearchApiException) {
+ return;
+ }
+ if (!($entity instanceof EntityInterface)) {
+ return;
+ }
+
+ if (in_array($item->getDatasource()->getEntityTypeId(), static::ENTITY_TYPES)) {
+ $this->doAddNodeField($item, $entity);
+ }
+ else {
+ $this->doAddEmbargoField($item, $entity);
+ }
+
+ }
+
+ /**
+ * Find the nodes related to the given entity.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity in question.
+ *
+ * @return string[]|int[]
+ * The IDs of the related nodes.
+ */
+ protected function findRelatedNodes(EntityInterface $entity) : array {
+ if ($entity->getEntityTypeId() === 'node') {
+ return [$entity->id()];
+ }
+ else {
+ $column = match ($entity->getEntityTypeId()) {
+ 'media' => 'mid',
+ 'file' => 'fid',
+ };
+ return $this->database->select(LUTGeneratorInterface::TABLE_NAME, 'lut')
+ ->fields('lut', ['nid'])
+ ->condition("lut.{$column}", $entity->id())
+ ->execute()
+ ->fetchCol();
+ }
+ }
+
+ /**
+ * Helper; build out field(s) for general content entities.
+ *
+ * @param \Drupal\search_api\Item\ItemInterface $item
+ * The item being indexed.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The content entity of the item being indexed.
+ */
+ protected function doAddNodeField(ItemInterface $item, EntityInterface $entity) : void {
+ $embargo_node_fields = $this->getFieldsHelper()->filterForPropertyPath($item->getFields(FALSE), NULL, static::NODE_FIELD);
+ if ($embargo_node_fields) {
+ $nodes = array_unique($this->findRelatedNodes($entity));
+
+ foreach ($embargo_node_fields as $field) {
+ foreach ($nodes as $node_id) {
+ $field->addValue($node_id);
+ }
+ }
+ }
+ }
+
+ /**
+ * Helper; build out field(s) for embargo entities, specifically.
+ *
+ * @param \Drupal\search_api\Item\ItemInterface $item
+ * The item being indexed.
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The content entity of the item being indexed.
+ */
+ protected function doAddEmbargoField(ItemInterface $item, EntityInterface $entity) : void {
+ assert($entity instanceof EmbargoInterface);
+ $paths = match ($entity->getEmbargoType()) {
+ EmbargoInterface::EMBARGO_TYPE_FILE => [static::EMBARGO_FIELD_FILE],
+ EmbargoInterface::EMBARGO_TYPE_NODE => [static::EMBARGO_FIELD_NODE, static::EMBARGO_FIELD_FILE],
+ };
+
+ $fields = $item->getFields(FALSE);
+ foreach ($paths as $path) {
+ $target_fields = $this->getFieldsHelper()->filterForPropertyPath($fields, NULL, $path);
+ foreach ($target_fields as $target_field) {
+ $target_field->addValue($entity->getEmbargoedNode()->id());
+ }
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function preIndexSave() : void {
+ $this->ensureField(NULL, static::NODE_FIELD, 'integer');
+ $this->ensureField(NULL, static::EMBARGO_FIELD_FILE, 'integer');
+ $this->ensureField(NULL, static::EMBARGO_FIELD_NODE, 'integer');
+
+ $this->ensureField('entity:embargo', 'id', 'integer');
+ $this->ensureField('entity:embargo', 'embargoed_node:entity:nid', 'integer');
+ $this->ensureField('entity:embargo', 'embargo_type', 'integer');
+ $this->ensureField('entity:embargo', 'expiration_date', 'date');
+ $this->ensureField('entity:embargo', 'expiration_type', 'integer');
+ $this->ensureField('entity:embargo', 'exempt_ips:entity:id', 'integer');
+ $this->ensureField('entity:embargo', 'exempt_users:entity:uid', 'integer');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) : void {
+ assert($query instanceof RefinableCacheableDependencyInterface);
+ $query->addCacheContexts(['user.permissions']);
+ if ($this->currentUser->hasPermission('bypass embargo access')) {
+ return;
+ }
+
+ $queries = [];
+
+ if (in_array('entity:node', $this->index->getDatasourceIds())) {
+ $queries['node'] = [
+ 'data sources' => ['entity:node'],
+ 'embargo path' => static::EMBARGO_FIELD_NODE,
+ 'node path' => static::NODE_FIELD,
+ ];
+ }
+ if ($intersection = array_intersect($this->index->getDatasourceIds(), ['entity:media', 'entity:file'])) {
+ $queries['file'] = [
+ 'data sources' => $intersection,
+ 'embargo path' => static::EMBARGO_FIELD_FILE,
+ 'node path' => static::NODE_FIELD,
+ ];
+ }
+
+ if (!$queries) {
+ return;
+ }
+
+ /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */
+ $ip_range_entities = $this->entityTypeManager->getStorage('embargo_ip_range')
+ ->getApplicableIpRanges($this->requestStack->getCurrentRequest()->getClientIp());
+
+ $query->addCacheContexts([
+ // Caching by groups of ranges instead of individually should promote
+ // cacheability.
+ 'ip.embargo_range',
+ // Exemptable users, so need to deal with them.
+ 'user.embargo__has_exemption',
+ ]);
+ // Embargo dates deal with granularity to the day.
+ $query->mergeCacheMaxAge(24 * 3600);
+
+ $types = ['embargo', 'embargo_ip_range', 'media', 'file', 'node'];
+ foreach ($types as $type) {
+ /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */
+ $entity_type = $this->entityTypeManager->getDefinition($type);
+ $query->addCacheTags($entity_type->getListCacheTags());
+ }
+
+ $query->addTag('embargo_join_processor');
+ $query->setOption('embargo_join_processor__ip_ranges', $ip_range_entities);
+ $query->setOption('embargo_join_processor__queries', $queries);
+ }
+
+}
diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php
new file mode 100644
index 0000000..6d31b28
--- /dev/null
+++ b/src/Plugin/search_api/processor/EmbargoProcessor.php
@@ -0,0 +1,297 @@
+requestStack = $container->get('request_stack');
+ $instance->entityTypeManager = $container->get('entity_type.manager');
+ $instance->currentUser = $container->get('current_user');
+ $instance->time = $container->get('datetime.time');
+
+ return $instance;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array {
+ $properties = [];
+
+ if ($datasource === NULL) {
+ return $properties;
+ }
+
+ $properties['embargo'] = ListableEntityProcessorProperty::create('embargo')
+ ->setList()
+ ->setProcessorId($this->getPluginId());
+
+ return $properties;
+ }
+
+ /**
+ * {@inheritdoc}
+ *
+ * Adapted from search_api's reverse_entity_references processor.
+ *
+ * @see \Drupal\search_api\Plugin\search_api\processor\ReverseEntityReferences::addFieldValues()
+ */
+ public function addFieldValues(ItemInterface $item) : void {
+ if (!in_array($item->getDatasource()->getEntityTypeId(), static::ENTITY_TYPES)) {
+ return;
+ }
+ try {
+ $entity = $item->getOriginalObject()->getValue();
+ }
+ catch (SearchApiException) {
+ return;
+ }
+ if (!($entity instanceof EntityInterface)) {
+ return;
+ }
+
+ $datasource_id = $item->getDatasourceId();
+
+ /** @var \Drupal\search_api\Item\FieldInterface[][][] $to_extract */
+ $to_extract = [];
+ foreach ($item->getFields(FALSE) as $field) {
+ $property_path = $field->getPropertyPath();
+ [$direct, $nested] = Utility::splitPropertyPath($property_path, FALSE);
+ if ($field->getDatasourceId() === $datasource_id
+ && $direct === 'embargo') {
+ $to_extract[$nested][] = $field;
+ }
+ }
+
+ /** @var \Drupal\embargo\EmbargoStorageInterface $embargo_storage */
+ $embargo_storage = $this->entityTypeManager->getStorage('embargo');
+ $embargoes = $embargo_storage->getApplicableEmbargoes($entity);
+ $relevant_embargoes = array_filter(
+ $embargoes,
+ function (EmbargoInterface $embargo) use ($entity) {
+ return in_array($embargo->getEmbargoType(), match ($entity->getEntityTypeId()) {
+ 'file', 'media' => [EmbargoInterface::EMBARGO_TYPE_FILE, EmbargoInterface::EMBARGO_TYPE_NODE],
+ 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE],
+ });
+ }
+ );
+
+ foreach ($relevant_embargoes as $embargo) {
+ $this->getFieldsHelper()->extractFields($embargo->getTypedData(), $to_extract);
+ }
+
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function preIndexSave() : void {
+ parent::preIndexSave();
+
+ /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $field_manager */
+ $field_manager = \Drupal::service('entity_field.manager');
+ $base_field_definitions = $field_manager->getBaseFieldDefinitions('embargo');
+
+ $ensure_label = function (FieldInterface $field) use ($base_field_definitions) {
+ if ($field->getLabel() === NULL) {
+ $label_pieces = ['Embargo:'];
+
+ $path_components = explode(IndexInterface::PROPERTY_PATH_SEPARATOR, $field->getPropertyPath(), 3);
+ $base_field = $base_field_definitions[$path_components[1]];
+ $label_pieces[] = $base_field->getLabel();
+
+ if (is_a($base_field->getClass(), EntityReferenceFieldItemListInterface::class, TRUE)) {
+ $label_pieces[] = 'Entity';
+ $label_pieces[] = 'ID';
+ }
+ $field->setLabel(implode(' ', $label_pieces));
+ }
+ return $field;
+ };
+
+ foreach ($this->index->getDatasources() as $datasource_id => $datasource) {
+ if (!in_array($datasource->getEntityTypeId(), static::ENTITY_TYPES)) {
+ continue;
+ }
+
+ $fields = [
+ $this->ensureField($datasource_id, 'embargo:id', 'integer'),
+ $this->ensureField($datasource_id, 'embargo:embargo_type', 'integer'),
+ $this->ensureField($datasource_id, 'embargo:expiration_date', 'date'),
+ $this->ensureField($datasource_id, 'embargo:expiration_type', 'integer'),
+ $this->ensureField($datasource_id, 'embargo:exempt_ips:entity:id', 'integer'),
+ $this->ensureField($datasource_id, 'embargo:exempt_users:entity:uid', 'integer'),
+ ];
+ array_map($ensure_label, $fields);
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function preprocessSearchQuery(QueryInterface $query) : void {
+ assert($query instanceof RefinableCacheableDependencyInterface);
+ $query->addCacheContexts(['user.permissions']);
+ if ($this->currentUser->hasPermission('bypass embargo access')) {
+ return;
+ }
+
+ $datasources = $query->getIndex()->getDatasources();
+ /** @var \Drupal\search_api\Datasource\DatasourceInterface[] $applicable_datasources */
+ $applicable_datasources = array_filter($datasources, function (DatasourceInterface $datasource) {
+ return in_array($datasource->getEntityTypeId(), static::ENTITY_TYPES);
+ });
+ if (empty($applicable_datasources)) {
+ 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 {
+ assert($query instanceof RefinableCacheableDependencyInterface);
+ $or_group = $query->createConditionGroup('OR', [
+ "embargo:$datasource_id",
+ ]);
+
+ // No embargo.
+ if ($field = $this->findField($datasource_id, 'embargo:id')) {
+ $or_group->addCondition($field->getFieldIdentifier(), NULL);
+ $query->addCacheTags(['embargo_list']);
+ }
+
+ // Embargo duration/schedule.
+ if ($expiration_type_field = $this->findField($datasource_id, 'embargo:expiration_type')) {
+ $schedule_group = $query->createConditionGroup(tags: ['embargo_schedule']);
+ // No indefinite embargo.
+ $schedule_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:expiration_date')) {
+ $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');
+ // Cacheable up to a day.
+ $query->mergeCacheMaxAge(24 * 3600);
+ }
+
+ $or_group->addConditionGroup($schedule_group);
+ }
+
+ if ($this->currentUser->isAnonymous()) {
+ $query->addCacheContexts(['user.roles:anonymous']);
+ }
+ elseif ($field = $this->findField($datasource_id, 'embargo:exempt_users:entity:uid')) {
+ $or_group->addCondition($field->getFieldIdentifier(), $this->currentUser->id());
+ $query->addCacheContexts(['user']);
+ }
+
+ if ($field = $this->findField($datasource_id, 'embargo: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()
+ ->getClientIp()) as $ipRange) {
+ $or_group->addCondition($field->getFieldIdentifier(), $ipRange->id());
+ $query->addCacheableDependency($ipRange);
+ }
+ $query->addCacheContexts(['ip.embargo_range']);
+ }
+
+ return (count($or_group->getConditions()) > 0) ? $or_group : NULL;
+ }
+
+}
diff --git a/src/Plugin/search_api/processor/Property/ListableEntityProcessorProperty.php b/src/Plugin/search_api/processor/Property/ListableEntityProcessorProperty.php
new file mode 100644
index 0000000..98e3a13
--- /dev/null
+++ b/src/Plugin/search_api/processor/Property/ListableEntityProcessorProperty.php
@@ -0,0 +1,34 @@
+definition['is_list'] = $value;
+ return $this;
+ }
+
+ /**
+ * Set the processor ID.
+ *
+ * @param string $processor_id
+ * The processor ID to set.
+ */
+ public function setProcessorId(string $processor_id) : self {
+ $this->definition['processor_id'] = $processor_id;
+ return $this;
+ }
+
+}
diff --git a/src/SearchApiTracker.php b/src/SearchApiTracker.php
new file mode 100644
index 0000000..995fabf
--- /dev/null
+++ b/src/SearchApiTracker.php
@@ -0,0 +1,285 @@
+get('module_handler'),
+ $container->get('search_api.entity_datasource.tracking_manager', ContainerInterface::NULL_ON_INVALID_REFERENCE),
+ $container->get('search_api.tracking_helper', ContainerInterface::NULL_ON_INVALID_REFERENCE),
+ $container->get('entity_type.manager'),
+ $container->get('database'),
+ );
+ }
+
+ /**
+ * Memoize if we found an index requiring our index maintenance.
+ *
+ * @var bool
+ */
+ protected bool $isProcessorEnabled;
+
+ /**
+ * Helper; determine if our "embargo_processor" processor is enabled.
+ *
+ * If _not_ enabled, we do not have to perform the index maintenance in this
+ * service.
+ *
+ * @return bool
+ * TRUE if the "embargo_processor" processor is enabled on an index;
+ * otherwise, FALSE.
+ */
+ protected function isProcessorEnabled() : bool {
+ if (!isset($this->isProcessorEnabled)) {
+ $this->isProcessorEnabled = FALSE;
+ if (!$this->moduleHandler->moduleExists('search_api')) {
+ return $this->isProcessorEnabled;
+ }
+ /** @var \Drupal\search_api\IndexInterface[] $indexes */
+ $indexes = $this->entityTypeManager->getStorage('search_api_index')
+ ->loadMultiple();
+ foreach ($indexes as $index) {
+ if ($index->isValidProcessor('embargo_processor')) {
+ $this->isProcessorEnabled = TRUE;
+ break;
+ }
+ }
+ }
+
+ return $this->isProcessorEnabled;
+ }
+
+ /**
+ * Track the given entity (and related entities) for indexing.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to track.
+ */
+ public function track(EntityInterface $entity) : void {
+ assert($entity instanceof EmbargoInterface);
+ if (!$this->isProcessorEnabled()) {
+ return;
+ }
+
+ // On updates, deal with the original value, in addition to the new.
+ if (isset($entity->original)) {
+ $this->track($entity->original);
+ }
+
+ if (!($node = $entity->getEmbargoedNode())) {
+ // No embargoed node?
+ return;
+ }
+
+ assert($node instanceof NodeInterface);
+
+ $this->doTrack($node);
+ $this->propagateChildren($node);
+ }
+
+ /**
+ * Actually deal with updating search_api's trackers.
+ *
+ * @param \Drupal\Core\Entity\ContentEntityInterface $entity
+ * The entity to track.
+ */
+ public function doTrack(ContentEntityInterface $entity) : void {
+ if (!$this->isProcessorEnabled()) {
+ return;
+ }
+ $this->trackingManager->trackEntityChange($entity);
+ $this->trackingHelper->trackReferencedEntityUpdate($entity);
+ }
+
+ /**
+ * Helper; propagate tracking updates down to related media and files.
+ *
+ * @param \Drupal\node\NodeInterface $node
+ * The node of which to propagate.
+ */
+ public function propagateChildren(NodeInterface $node) : void {
+ $results = $this->database->select(LUTGeneratorInterface::TABLE_NAME, 'lut')
+ ->fields('lut', ['mid', 'fid'])
+ ->condition('nid', $node->id())
+ ->execute();
+ $media_ids = array_unique($results->fetchCol(/* 0 */));
+ $file_ids = array_unique($results->fetchCol(1));
+
+ /** @var \Drupal\media\MediaInterface $media */
+ foreach ($this->entityTypeManager->getStorage('media')->loadMultiple($media_ids) as $media) {
+ $this->doTrack($media);
+ }
+ /** @var \Drupal\file\FileInterface $file */
+ foreach ($this->entityTypeManager->getStorage('file')->loadMultiple($file_ids) as $file) {
+ $this->doTrack($file);
+ }
+ }
+
+ /**
+ * Helper; get the media type with its specific interface.
+ *
+ * @param \Drupal\media\MediaInterface $media
+ * The media of which to get the type.
+ *
+ * @return \Drupal\media\MediaTypeInterface
+ * The media type of the given media.
+ */
+ protected function getMediaType(MediaInterface $media) : MediaTypeInterface {
+ $type = $this->entityTypeManager->getStorage('media_type')->load($media->bundle());
+ assert($type instanceof MediaTypeInterface);
+ return $type;
+ }
+
+ /**
+ * Determine if special tracking is required for this media.
+ *
+ * Given search_api indexes could be built specifically for files, we should
+ * reset any related tracking due to the islandora_hierarchical_access
+ * relations across the entity types.
+ *
+ * @param \Drupal\media\MediaInterface $media
+ * The media to test.
+ *
+ * @return bool
+ * TRUE if relevant; otherwise, FALSE.
+ */
+ public function isMediaRelevant(MediaInterface $media) : bool {
+ if (!$this->isProcessorEnabled()) {
+ return FALSE;
+ }
+ // No `field_media_of`, so unrelated to IHA LUT.
+ if (!$media->hasField(IslandoraUtils::MEDIA_OF_FIELD)) {
+ return FALSE;
+ }
+
+ $media_type = $this->getMediaType($media);
+ $media_source = $media->getSource();
+ if ($media_source->getSourceFieldDefinition($media_type)->getSetting('target_type') !== 'file') {
+ return FALSE;
+ }
+
+ return TRUE;
+ }
+
+ /**
+ * Get the file for the media.
+ *
+ * @param \Drupal\media\MediaInterface|null $media
+ * The media of which to get the file.
+ *
+ * @return \Drupal\file\FileInterface|null
+ * The file if it could be loaded; otherwise, NULL.
+ */
+ public function mediaGetFile(?MediaInterface $media) : ?FileInterface {
+ return $media ?
+ $this->entityTypeManager->getStorage('file')->load(
+ $media->getSource()->getSourceFieldValue($media)
+ ) :
+ NULL;
+ }
+
+ /**
+ * Helper; get the containing nodes.
+ *
+ * @param \Drupal\media\MediaInterface|null $media
+ * The media of which to enumerate the containing node(s).
+ *
+ * @return \Drupal\node\NodeInterface[]
+ * The containing node(s).
+ */
+ protected function getMediaContainers(?MediaInterface $media) : array {
+ /** @var \Drupal\Core\Field\EntityReferenceFieldItemList|null $containers */
+ $containers = $media?->get(IslandoraUtils::MEDIA_OF_FIELD);
+ $entities = $containers?->referencedEntities() ?? [];
+ $to_return = [];
+ foreach ($entities as $entity) {
+ $to_return[$entity->id()] = $entity;
+ }
+ return $to_return;
+ }
+
+ /**
+ * React to media create/update events.
+ *
+ * @param \Drupal\media\MediaInterface $media
+ * The media being operated on.
+ */
+ public function mediaWriteReaction(MediaInterface $media) : void {
+ if (!$this->isMediaRelevant($media)) {
+ return;
+ }
+
+ $original_file = $this->mediaGetFile($media->original ?? NULL);
+ $current_file = $this->mediaGetFile($media);
+
+ $same_file = $original_file === $current_file;
+
+ $original_containers = $this->getMediaContainers($media->original ?? NULL);
+ $current_containers = $this->getMediaContainers($media);
+
+ $same_containers = $current_containers == array_intersect_key($current_containers, $original_containers);
+
+ if (!($same_file && $same_containers)) {
+ if ($original_file) {
+ $this->doTrack($original_file);
+ }
+ if ($current_file) {
+ $this->doTrack($current_file);
+ }
+ }
+ }
+
+ /**
+ * React to media delete events.
+ *
+ * @param \Drupal\media\MediaInterface $media
+ * The media entity that is/was being deleted.
+ */
+ public function mediaDeleteReaction(MediaInterface $media) : void {
+ if (!$this->isMediaRelevant($media)) {
+ return;
+ }
+
+ if ($current_file = $this->mediaGetFile($media)) {
+ $this->doTrack($current_file);
+ }
+ }
+
+}
diff --git a/templates/embargo-notification.html.twig b/templates/embargo-notification.html.twig
index d1c7bb8..633852a 100644
--- a/templates/embargo-notification.html.twig
+++ b/templates/embargo-notification.html.twig
@@ -37,7 +37,7 @@
restricted indefinitely.
{% endtrans %}
{% else %}
- {% set expiry_date = embargo.expiration_date.getPhpDateTime|date('Y-m-d') %}
+ {% set expiry_date = embargo.actual.expiration_date.date.getPhpDateTime|date('Y-m-d') %}
{% trans %}
Access to all associated files of this resource is
restricted until
@@ -51,7 +51,7 @@
restricted indefinitely.
{% endtrans %}
{% else %}
- {% set expiry_date = embargo.expiration_date.getPhpDateTime|date('Y-m-d') %}
+ {% set expiry_date = embargo.actual.expiration_date.date.getPhpDateTime|date('Y-m-d') %}
{% trans %}
Access to this resource and all associated files is
restricted until
diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php
index c30857f..e1d3703 100644
--- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php
+++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php
@@ -3,6 +3,10 @@
namespace Drupal\Tests\embargo\Kernel;
use Drupal\embargo\EmbargoInterface;
+use Drupal\file\FileInterface;
+use Drupal\media\Entity\Media;
+use Drupal\media\MediaInterface;
+use Drupal\node\NodeInterface;
use Drupal\Tests\islandora_test_support\Traits\DatabaseQueryTestTraits;
/**
@@ -11,20 +15,120 @@
* @group embargo
*/
class EmbargoAccessQueryTaggingAlterTest extends EmbargoKernelTestBase {
+
use DatabaseQueryTestTraits;
+ /**
+ * Test embargo instance.
+ *
+ * @var \Drupal\embargo\EmbargoInterface
+ */
+ protected EmbargoInterface $embargo;
+
+ /**
+ * Embargoed node from ::setUp().
+ *
+ * @var \Drupal\node\NodeInterface
+ */
+ protected NodeInterface $embargoedNode;
+
+ /**
+ * Embargoed media from ::setUp().
+ *
+ * @var \Drupal\media\MediaInterface
+ */
+ protected MediaInterface $embargoedMedia;
+
+ /**
+ * Embargoed file from ::setUp().
+ *
+ * @var \Drupal\file\FileInterface
+ */
+ protected FileInterface $embargoedFile;
+
+ /**
+ * Unembargoed node from ::setUp().
+ *
+ * @var \Drupal\node\NodeInterface
+ */
+ protected NodeInterface $unembargoedNode;
+
+ /**
+ * Unembargoed media from ::setUp().
+ *
+ * @var \Drupal\media\MediaInterface
+ */
+ protected MediaInterface $unembargoedMedia;
+
+ /**
+ * Unembargoed file from ::setUp().
+ *
+ * @var \Drupal\file\FileInterface
+ */
+ protected FileInterface $unembargoedFile;
+
+ /**
+ * Unassociated node from ::setUp().
+ *
+ * @var \Drupal\node\NodeInterface
+ */
+ protected NodeInterface $unassociatedNode;
+
+ /**
+ * Unassociated media from ::setUp().
+ *
+ * @var \Drupal\media\MediaInterface
+ */
+ protected MediaInterface $unassociatedMedia;
+
+ /**
+ * Unassociated file from ::setUp().
+ *
+ * @var \Drupal\file\FileInterface
+ */
+ protected FileInterface $unassociatedFile;
+
+ /**
+ * Lazily created "default thumbnail" image file for (file) media.
+ *
+ * @var \Drupal\file\FileInterface
+ * @see https://git.drupalcode.org/project/drupal/-/blob/cd2c8e49c861a70b0f39b17c01051b16fd6a2662/core/modules/media/src/Entity/Media.php#L203-208
+ */
+ protected FileInterface $mediaTypeDefaultFile;
+
/**
* {@inheritdoc}
*/
public function setUp(): void {
parent::setUp();
+ $this->setupEntities();
+ }
+
+ /**
+ * Helper; build out entities with which to test.
+ */
+ protected function setupEntities() : void {
// Create two nodes one embargoed and one non-embargoed.
- $embargoedNode = $this->createNode();
- $this->createMedia($this->createFile(), $embargoedNode);
- $this->embargo = $this->createEmbargo($embargoedNode);
+ $this->embargoedNode = $this->createNode();
+ $this->embargoedMedia = $this->createMedia($this->embargoedFile = $this->createFile(), $this->embargoedNode);
+ $this->embargo = $this->createEmbargo($this->embargoedNode);
- $this->createNode();
+ $this->unembargoedNode = $this->createNode();
+ $this->unembargoedMedia = $this->createMedia($this->unembargoedFile = $this->createFile(), $this->unembargoedNode);
+
+ $this->unassociatedNode = $this->createNode();
+ $this->unassociatedMedia = Media::create([
+ 'bundle' => $this->createMediaType('file', ['id' => 'file_two'])->id(),
+ ])->setPublished();
+ $this->unassociatedMedia->save();
+ $this->unassociatedFile = $this->createFile();
+
+ // XXX: Media lazily creates a "default thumbnail" image file by default.
+ // @see https://git.drupalcode.org/project/drupal/-/blob/cd2c8e49c861a70b0f39b17c01051b16fd6a2662/core/modules/media/src/Entity/Media.php#L203-208
+ $files = $this->storage('file')->loadByProperties(['filename' => 'generic.png']);
+ $this->assertCount(1, $files, 'only the one generic file.');
+ $this->mediaTypeDefaultFile = reset($files);
}
/**
@@ -35,7 +139,11 @@ public function setUp(): void {
public function testEmbargoNodeQueryAlterAccess() {
$query = $this->generateNodeSelectAccessQuery($this->user);
$result = $query->execute()->fetchAll();
- $this->assertCount(1, $result, 'User can only view non-embargoed node.');
+
+ $ids = array_column($result, 'nid');
+ $this->assertNotContains($this->embargoedNode->id(), $ids, 'does not contain embargoed node');
+ $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node');
+ $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node');
}
/**
@@ -46,7 +154,11 @@ public function testEmbargoNodeQueryAlterAccess() {
public function testNodeEmbargoReferencedMediaAccessQueryAlterAccessDenied() {
$query = $this->generateMediaSelectAccessQuery($this->user);
$result = $query->execute()->fetchAll();
- $this->assertCount(0, $result, 'Media of embargoed nodes cannot be viewed');
+
+ $ids = array_column($result, 'mid');
+ $this->assertNotContains($this->embargoedMedia->id(), $ids, 'does not contain embargoed media');
+ $this->assertContains($this->unembargoedMedia->id(), $ids, 'contains unembargoed media');
+ $this->assertContains($this->unassociatedMedia->id(), $ids, 'contains unassociated media');
}
/**
@@ -57,7 +169,12 @@ public function testNodeEmbargoReferencedMediaAccessQueryAlterAccessDenied() {
public function testNodeEmbargoReferencedFileAccessQueryAlterAccessDenied() {
$query = $this->generateFileSelectAccessQuery($this->user);
$result = $query->execute()->fetchAll();
- $this->assertCount(1, $result, 'File of embargoed nodes cannot be viewed');
+
+ $ids = array_column($result, 'fid');
+ $this->assertNotContains($this->embargoedFile->id(), $ids, 'does not contain embargoed file');
+ $this->assertContains($this->unembargoedFile->id(), $ids, 'contains unembargoed file');
+ $this->assertContains($this->unassociatedFile->id(), $ids, 'contains unassociated file');
+ $this->assertContains($this->mediaTypeDefaultFile->id(), $ids, 'contains default mediatype file');
}
/**
@@ -72,7 +189,10 @@ public function testDeletedNodeEmbargoNodeAccessQueryAlterAccessAllowed() {
$query = $this->generateNodeSelectAccessQuery($this->user);
$result = $query->execute()->fetchAll();
- $this->assertCount(2, $result, 'Non embargoed nodes can be viewed');
+ $ids = array_column($result, 'nid');
+ $this->assertContains($this->embargoedNode->id(), $ids, 'contains formerly embargoed node');
+ $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node');
+ $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node');
}
/**
@@ -86,8 +206,11 @@ public function testDeletedNodeEmbargoMediaAccessQueryAlterAccessAllowed() {
$this->embargo->delete();
$query = $this->generateMediaSelectAccessQuery($this->user);
$result = $query->execute()->fetchAll();
- $this->assertCount(1, $result,
- 'Media of non embargoed nodes can be viewed');
+
+ $ids = array_column($result, 'mid');
+ $this->assertContains($this->embargoedMedia->id(), $ids, 'contains formerly embargoed media');
+ $this->assertContains($this->unembargoedMedia->id(), $ids, 'contains unembargoed media');
+ $this->assertContains($this->unassociatedMedia->id(), $ids, 'contains unassociated media');
}
/**
@@ -99,11 +222,15 @@ public function testDeletedNodeEmbargoMediaAccessQueryAlterAccessAllowed() {
*/
public function testDeletedNodeEmbargoFileAccessQueryAlterAccessAllowed() {
$this->embargo->delete();
- $query = $this->generateFileSelectAccessQuery($this->user);
+ $query = $this->generateFileSelectAccessQuery($this->user);
$result = $query->execute()->fetchAll();
- $this->assertCount(2, $result,
- 'Files of non embargoed nodes can be viewed');
+
+ $ids = array_column($result, 'fid');
+ $this->assertContains($this->embargoedFile->id(), $ids, 'contains formerly embargoed file');
+ $this->assertContains($this->unembargoedFile->id(), $ids, 'contains unembargoed file');
+ $this->assertContains($this->unassociatedFile->id(), $ids, 'contains unassociated file');
+ $this->assertContains($this->mediaTypeDefaultFile->id(), $ids, 'contains default mediatype file');
}
/**
@@ -112,16 +239,19 @@ public function testDeletedNodeEmbargoFileAccessQueryAlterAccessAllowed() {
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function testPublishScheduledEmbargoAccess() {
- // Create an embargo scheduled to be unpublished in the future.
+ // Create an embargo scheduled to be published in the future.
$this->setEmbargoFutureUnpublishDate($this->embargo);
- $nodeCount = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll();
- $this->assertCount(1, $nodeCount,
- 'Node is still embargoed.');
+ $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll();
+
+ $ids = array_column($result, 'nid');
+ $this->assertNotContains($this->embargoedNode->id(), $ids, 'does not contain embargoed node');
+ $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node');
+ $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node');
}
/**
- * Tests embargo scheduled to be unpublished in the past.
+ * Test embargo scheduled in the past, without any other embargo.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
@@ -130,9 +260,54 @@ public function testUnpublishScheduledEmbargoAccess() {
// Create an embargo scheduled to be unpublished in the future.
$this->setEmbargoPastUnpublishDate($this->embargo);
- $nodeCount = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll();
- $this->assertCount(2, $nodeCount,
- 'Embargo has been unpublished.');
+ $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll();
+
+ $ids = array_column($result, 'nid');
+ $this->assertContains($this->embargoedNode->id(), $ids, 'contains node with expired embargo');
+ $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node');
+ $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node');
+ }
+
+ /**
+ * Test embargo scheduled in the past with another relevant scheduled embargo.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ */
+ public function testUnpublishScheduledWithPublishedEmbargoAccess() {
+ $this->embargo->setExpirationType(EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)->save();
+ // Create an embargo scheduled to be unpublished in the future.
+ $this->setEmbargoPastUnpublishDate($this->embargo);
+
+ $embargo = $this->createEmbargo($this->embargoedNode);
+ $embargo->setExpirationType(EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)->save();
+ $this->setEmbargoFutureUnpublishDate($embargo);
+
+ $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll();
+
+ $ids = array_column($result, 'nid');
+ $this->assertNotContains($this->embargoedNode->id(), $ids, 'does not contain node with expired embargo having other schedule embargo in future');
+ $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node');
+ $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node');
+ }
+
+ /**
+ * Test embargo scheduled in the past, but with a separate indefinite embargo.
+ *
+ * @throws \Drupal\Core\Entity\EntityStorageException
+ */
+ public function testUnpublishScheduledWithIndefiniteEmbargoAccess() {
+ $this->embargo->setExpirationType(EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)->save();
+ // Create an embargo scheduled to be unpublished in the future.
+ $this->setEmbargoPastUnpublishDate($this->embargo);
+
+ $this->createEmbargo($this->embargoedNode);
+
+ $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll();
+
+ $ids = array_column($result, 'nid');
+ $this->assertNotContains($this->embargoedNode->id(), $ids, 'does not contain node with expired embargo having other indefinite embargo');
+ $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node');
+ $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node');
}
}
diff --git a/tests/src/Kernel/EmbargoKernelTestBase.php b/tests/src/Kernel/EmbargoKernelTestBase.php
index c79114b..ce81111 100644
--- a/tests/src/Kernel/EmbargoKernelTestBase.php
+++ b/tests/src/Kernel/EmbargoKernelTestBase.php
@@ -2,9 +2,9 @@
namespace Drupal\Tests\embargo\Kernel;
-use Drupal\embargo\Entity\IpRange;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\embargo\EmbargoInterface;
+use Drupal\embargo\Entity\IpRange;
use Drupal\embargo\IpRangeInterface;
use Drupal\node\NodeInterface;
use Drupal\Tests\islandora_test_support\Kernel\AbstractIslandoraKernelTestBase;
diff --git a/tests/src/Kernel/FileEmbargoTest.php b/tests/src/Kernel/FileEmbargoTest.php
index de399fb..90d09cf 100644
--- a/tests/src/Kernel/FileEmbargoTest.php
+++ b/tests/src/Kernel/FileEmbargoTest.php
@@ -73,7 +73,7 @@ public function testEmbargoedNodeRelatedMediaFileAccessDenied($operation) {
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function testDeletedEmbargoedFileRelatedMediaFileAccessAllowed(
- $operation
+ $operation,
) {
$node = $this->createNode();
$file = $this->createFile();
diff --git a/tests/src/Kernel/IpRangeEmbargoTest.php b/tests/src/Kernel/IpRangeEmbargoTest.php
index 9b29a0d..a858416 100644
--- a/tests/src/Kernel/IpRangeEmbargoTest.php
+++ b/tests/src/Kernel/IpRangeEmbargoTest.php
@@ -2,9 +2,10 @@
namespace Drupal\Tests\embargo\Kernel;
-use Drupal\node\NodeInterface;
use Drupal\embargo\EmbargoInterface;
use Drupal\embargo\IpRangeInterface;
+use Drupal\node\NodeInterface;
+use Drupal\Tests\islandora_test_support\Traits\DatabaseQueryTestTraits;
/**
* Test IpRange embargo.
@@ -13,6 +14,8 @@
*/
class IpRangeEmbargoTest extends EmbargoKernelTestBase {
+ use DatabaseQueryTestTraits;
+
/**
* Embargo for test.
*
@@ -105,8 +108,16 @@ public function setUp(): void {
$this->embargoedNodeWithDifferentIpRange = $this->createNode();
$this->currentIpRangeEntity = $this->createIpRangeEntity($this->ipRange);
$this->embargoWithoutIpRange = $this->createEmbargo($this->embargoedNodeWithoutIpRange);
- $this->embargoWithCurrentIpRange = $this->createEmbargo($this->embargoedNodeWithCurrentIpRange, 1, $this->currentIpRangeEntity);
- $this->embargoWithDifferentIpRange = $this->createEmbargo($this->embargoedNodeWithDifferentIpRange, 1, $this->createIpRangeEntity('0.0.0.0.1/29'));
+ $this->embargoWithCurrentIpRange = $this->createEmbargo(
+ $this->embargoedNodeWithCurrentIpRange,
+ EmbargoInterface::EMBARGO_TYPE_NODE,
+ $this->currentIpRangeEntity,
+ );
+ $this->embargoWithDifferentIpRange = $this->createEmbargo(
+ $this->embargoedNodeWithDifferentIpRange,
+ EmbargoInterface::EMBARGO_TYPE_NODE,
+ $this->createIpRangeEntity('0.0.0.1/29'),
+ );
}
/**
@@ -127,4 +138,18 @@ public function testIpRangeEmbargoNodeAccess() {
$this->assertTrue($this->embargoedNodeWithCurrentIpRange->access('view', $this->user));
}
+ /**
+ * Test IP range query tagging.
+ */
+ public function testIpRangeQueryTagging() {
+ $results = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll();
+
+ $ids = array_column($results, 'nid');
+
+ $this->assertContains($this->nonEmbargoedNode->id(), $ids, 'non-embargoed node present');
+ $this->assertNotContains($this->embargoedNodeWithoutIpRange->id(), $ids, 'generally embargoed node absent');
+ $this->assertNotContains($this->embargoedNodeWithDifferentIpRange->id(), $ids, 'node exempted to other ranges absent');
+ $this->assertContains($this->embargoedNodeWithCurrentIpRange->id(), $ids, 'node exempted to our range present');
+ }
+
}