From b6219fee4b195f2af320726f56ed7770ca802606 Mon Sep 17 00:00:00 2001 From: Prashant Kanse Date: Fri, 2 Feb 2024 20:46:59 +0530 Subject: [PATCH 01/82] DGIR-150 : Processor to add data in indexing --- src/Plugin/search_api/.DS_Store | Bin 0 -> 6148 bytes .../search_api/processor/EmbargoProcessor.php | 133 ++++++++++++++++++ 2 files changed, 133 insertions(+) create mode 100644 src/Plugin/search_api/.DS_Store create mode 100644 src/Plugin/search_api/processor/EmbargoProcessor.php diff --git a/src/Plugin/search_api/.DS_Store b/src/Plugin/search_api/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..b6d378b133c81f9be12f995d5e059ee8ddd4090c GIT binary patch literal 6148 zcmeHKI}XAy47Gs)#L|&5SKtN_f*pwqAU+#XB@lZj&c)Gq{tTeP1`B$woR>I$srrVv z77<-s4|9=5M81IC@mTFzkYpZWXKu5C}Y zO``%-fC^9nDnJFks6ZCi@$`#l@+c}m1^z<;yB`YNuqJkaesy5*763Rx*bQ^pyTG7}X7i!>X3Y*o{p~ovc)Dl}i80h5~3oFMnPl~)^ YbL`i|F3{#>1%9o-1B6}`tpET3 literal 0 HcmV?d00001 diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php new file mode 100644 index 0000000..7e6862b --- /dev/null +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -0,0 +1,133 @@ +entityTypeManager = $entity_type_manager; + $this->storage = $entity_type_manager->getStorage('embargo'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) { + $properties = []; + + if ($datasource) { + $definition = [ + 'label' => $this->t('Embargo Status'), + 'description' => $this->t('Field to pass embargo status to solr index.'), + 'type' => 'string', + 'processor_id' => $this->getPluginId(), + 'is_list' => TRUE, + ]; + $properties['embargo_status'] = new CustomValueProperty($definition); + } + + return $properties; + } + + /** + * {@inheritdoc} + */ + public function addFieldValues(ItemInterface $item) { + $embargo_expiration_type = $embargo_type = []; + + // Get node ID. + $item_id = $item->getId(); + $nodeId = preg_replace('/^entity:node\/(\d+):en$/', '$1', $item_id); + + // Load node based on ID, and get embargo. + $node = $this->entityTypeManager->getStorage('node')->load($nodeId); + $storages = $this->storage->getApplicableEmbargoes($node); + + if (empty($storages)) { + return; + } + + // Get Embargo details and prepare to pass it to index field. + foreach ($storages as $embargo) { + $allowed_expiration_types = EmbargoInterface::EXPIRATION_TYPES; + $allowed_embargo_type = EmbargoInterface::EMBARGO_TYPES; + // Get Embargo Type. + $embargo_type[] = 'embargo-type-' . $allowed_embargo_type[$embargo->getEmbargoType()]; + + // Get Expiration Type. + $embargo_expiration_type[] = 'embargo-expiration-type-' . $allowed_expiration_types[$embargo->getExpirationType()]; + } + + $embargo_status = $combinedString = implode(' ', $embargo_type) . ' ' . implode(' ', $embargo_expiration_type); + + $fields = $this->getFieldsHelper() + ->filterForPropertyPath($item->getFields(), $item->getDatasourceId(), 'embargo_status'); + foreach ($fields as $field) { + $field->addValue($embargo_status); + } + } + +} From df8d21218818e43f29cdde44b2ca65124b577396 Mon Sep 17 00:00:00 2001 From: Prashant Kanse Date: Fri, 2 Feb 2024 20:47:45 +0530 Subject: [PATCH 02/82] DGIR-150 : Processor to add data in indexing --- src/Plugin/search_api/.DS_Store | Bin 6148 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/Plugin/search_api/.DS_Store diff --git a/src/Plugin/search_api/.DS_Store b/src/Plugin/search_api/.DS_Store deleted file mode 100644 index b6d378b133c81f9be12f995d5e059ee8ddd4090c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKI}XAy47Gs)#L|&5SKtN_f*pwqAU+#XB@lZj&c)Gq{tTeP1`B$woR>I$srrVv z77<-s4|9=5M81IC@mTFzkYpZWXKu5C}Y zO``%-fC^9nDnJFks6ZCi@$`#l@+c}m1^z<;yB`YNuqJkaesy5*763Rx*bQ^pyTG7}X7i!>X3Y*o{p~ovc)Dl}i80h5~3oFMnPl~)^ YbL`i|F3{#>1%9o-1B6}`tpET3 From 6d06aa3b80e767662317d0b5c223cc8c8280010b Mon Sep 17 00:00:00 2001 From: Prashant Kanse Date: Fri, 2 Feb 2024 20:50:48 +0530 Subject: [PATCH 03/82] DGIR-150 : Comment changes --- src/Plugin/search_api/processor/EmbargoProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 7e6862b..ad07ce2 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -12,7 +12,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** - * A search_api_solr processor to filter results based on embargo and user IP. + * A search_api_solr processor to add embargo related info. * * @SearchApiProcessor( * id = "embargo_processor", From 84dd7a5c9bac61b03c1ab06a2a147d7c0bd22e9c Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Sat, 17 Feb 2024 15:06:02 -0400 Subject: [PATCH 04/82] Rework querying. Should reduce the number of joins when dealing with various multiple file and media fields in the same query, as it instead builds out a disjuction in the "join" condition against the LUT. --- src/Access/QueryTagger.php | 101 +++++++++--------- .../EmbargoAccessQueryTaggingAlterTest.php | 7 ++ 2 files changed, 55 insertions(+), 53 deletions(-) diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index 81f9c58..009af6a 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -90,7 +90,7 @@ public function __construct( * @param string $type * Either "node" or "file". */ - public function tagAccess(SelectInterface $query, string $type) { + public function tagAccess(SelectInterface $query, string $type) : void { if (!in_array($type, ['node', 'media', 'file'])) { throw new \InvalidArgumentException("Unrecognized type '$type'."); } @@ -104,53 +104,61 @@ public function tagAccess(SelectInterface $query, string $type) { $storage = $this->entityTypeManager->getStorage($type); $tables = $storage->getTableMapping()->getTableNames(); + $target_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"); + $key = (str_starts_with($info['table'], "{$type}__")) ? 'entity_id' : (substr($type, 0, 1) . "id"); $alias = $info['alias']; - - $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'."); - } + $target_aliases[] = "{$alias}.{$key}"; } } - } - /** - * 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'); + if (empty($target_aliases)) { + return; + } + + $existence = $this->database->select('node', 'existence_node'); + $existence->fields('existence_node', ['nid']); + + if ($type !== 'node') { + $lut_column = match($type) { + 'file' => 'fid', + 'media' => 'mid', + }; + $existence_lut_alias = $existence->leftJoin(LUTGeneratorInterface::TABLE_NAME, 'lut', '%alias.nid = existence_node.nid'); + $existence->where(strtr('!field IS NULL OR !field IN (!targets)', [ + '!field' => "{$existence_lut_alias}.{$lut_column}", + '!targets' => implode(', ', $target_aliases), + ])); + } + else { + $existence->where(strtr('!field IN (!targets)', [ + '!field' => 'existence_node.nid', + '!targets' => implode(', ', $target_aliases), + ])); + } + + $exist_or = $existence->orConditionGroup(); + + // No embargo. + $embargo = $this->database->select('embargo', 'ee'); + $embargo->fields('ee', ['embargoed_node']); + $exist_or->condition("existence_node.nid", $embargo, 'NOT IN'); + + // Embargoed (and allowed). + $accessible_embargoes = $this->buildAccessibleEmbargoesQuery(match($type) { + 'file', 'media' => EmbargoInterface::EMBARGO_TYPE_FILE, + 'node' => EmbargoInterface::EMBARGO_TYPE_NODE, + }); + $exist_or->condition("existence_node.nid", $accessible_embargoes, 'IN'); + + $existence->condition($exist_or); + + $query->exists($existence); } /** @@ -164,7 +172,7 @@ protected function buildInaccessibleFileCondition(string $lut_column) { * @return \Drupal\Core\Database\Query\SelectInterface * A query returning things that should not be inaccessible. */ - protected function buildAccessibleEmbargoesQuery($type) : SelectInterface { + protected function buildAccessibleEmbargoesQuery(int $type) : SelectInterface { $query = $this->database->select('embargo', 'e') ->fields('e', ['embargoed_node']); @@ -197,17 +205,4 @@ protected function buildAccessibleEmbargoesQuery($type) : SelectInterface { 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']); - } - } diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php index c30857f..b053fd8 100644 --- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php +++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php @@ -13,6 +13,13 @@ class EmbargoAccessQueryTaggingAlterTest extends EmbargoKernelTestBase { use DatabaseQueryTestTraits; + /** + * Test embargo instance. + * + * @var \Drupal\embargo\EmbargoInterface + */ + protected EmbargoInterface $embargo; + /** * {@inheritdoc} */ From 3896ae6e3566acbfc7211773f0e2d5d086a0c0d7 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 20 Feb 2024 09:39:27 -0400 Subject: [PATCH 05/82] Move more explicitly to existence check. --- src/Access/QueryTagger.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index 009af6a..0de4128 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -147,14 +147,16 @@ public function tagAccess(SelectInterface $query, string $type) : void { // No embargo. $embargo = $this->database->select('embargo', 'ee'); $embargo->fields('ee', ['embargoed_node']); - $exist_or->condition("existence_node.nid", $embargo, 'NOT IN'); + $embargo->where('existence_node.nid = ee.embargoed_node'); + $exist_or->notExists($embargo); // Embargoed (and allowed). $accessible_embargoes = $this->buildAccessibleEmbargoesQuery(match($type) { 'file', 'media' => EmbargoInterface::EMBARGO_TYPE_FILE, 'node' => EmbargoInterface::EMBARGO_TYPE_NODE, }); - $exist_or->condition("existence_node.nid", $accessible_embargoes, 'IN'); + $accessible_embargoes->where('existence_node.nid = e.embargoed_node'); + $exist_or->exists($accessible_embargoes); $existence->condition($exist_or); From 18a56613cf38fde020befd7f265cef8e5f6fcca8 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 27 Feb 2024 17:52:10 -0400 Subject: [PATCH 06/82] Rework to add ALL the info. --- .../processor/EmbargoInfoProperty.php | 90 ++++++++++ .../search_api/processor/EmbargoProcessor.php | 157 ++++++++++-------- 2 files changed, 182 insertions(+), 65 deletions(-) create mode 100644 src/Plugin/search_api/processor/EmbargoInfoProperty.php diff --git a/src/Plugin/search_api/processor/EmbargoInfoProperty.php b/src/Plugin/search_api/processor/EmbargoInfoProperty.php new file mode 100644 index 0000000..41daca5 --- /dev/null +++ b/src/Plugin/search_api/processor/EmbargoInfoProperty.php @@ -0,0 +1,90 @@ +propertyDefinitions)) { + $this->propertyDefinitions = [ + 'total_count' => new ProcessorProperty([ + 'label' => $this->t('Applicable embargo count'), + 'description' => $this->t('Total number of applicable embargoes.'), + 'type' => 'int', + 'processor_id' => $this->getProcessorId(), + 'is_list' => FALSE, + 'computed' => FALSE, + ]), + 'indefinite_count' => new ProcessorProperty([ + 'label' => $this->t('Count of indefinite embargoes'), + 'description' => $this->t('Total number of indefinite embargoes.'), + 'type' => 'int', + 'processor_id' => $this->getProcessorId(), + 'is_list' => FALSE, + 'computed' => FALSE, + ]), + 'scheduled_timestamps' => new ProcessorProperty([ + 'label' => $this->t('Schedule embargo expiry timestamps'), + 'description' => $this->t('Unix timestamps when the embargoes expire.'), + 'type' => 'int', + 'processor_id' => $this->getProcessorId(), + 'is_list' => TRUE, + 'computed' => FALSE, + ]), + 'exempt_users' => new ProcessorProperty([ + 'label' => $this->t('IDs of users exempt from embargoes'), + 'description' => '', + 'type' => 'int', + 'processor_id' => $this->getProcessorId(), + 'is_list' => TRUE, + 'computed' => FALSE, + ]), + 'exempt_ip_ranges' => new ProcessorProperty([ + 'label' => $this->t('IP range entity IDs'), + 'description' => '', + 'type' => 'string', + 'processor_id' => $this->getProcessorId(), + 'is_list' => TRUE, + 'computed' => FALSE, + ]), + ]; + } + + return $this->propertyDefinitions; + } + + /** + * {@inheritDoc} + */ + public function getProcessorId() { + return $this->definition['processor_id']; + } + + /** + * {@inheritDoc} + */ + public function isHidden() { + return !empty($this->definition['hidden']); + } + + /** + * {@inheritdoc} + */ + public function isList() { + return (bool) ($this->definition['is_list'] ?? parent::isList()); + } + +} diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index ad07ce2..ac7c536 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -2,13 +2,18 @@ namespace Drupal\embargo\Plugin\search_api\processor; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\embargo\EmbargoInterface; +use Drupal\embargo\EmbargoStorageInterface; +use Drupal\embargo\Plugin\search_api\processor\Property\EmbargoInfoProperty; +use Drupal\file\FileInterface; +use Drupal\islandora\IslandoraUtils; +use Drupal\media\MediaInterface; +use Drupal\node\NodeInterface; use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\Item\ItemInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\search_api\Processor\ProcessorPluginBase; -use Drupal\search_api\Plugin\search_api\processor\Property\CustomValueProperty; -use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -26,107 +31,129 @@ * ) */ class EmbargoProcessor extends ProcessorPluginBase implements ContainerFactoryPluginInterface { - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; /** * Embargo entity storage. * * @var \Drupal\embargo\EmbargoStorageInterface */ - protected $storage; + protected EmbargoStorageInterface $storage; /** - * Constructs a new instance of the class. + * Islandora utils helper service. * - * @param array $configuration - * A configuration array containing information about the plugin instance. - * @param string $plugin_id - * The plugin_id for the plugin instance. - * @param mixed $plugin_definition - * The plugin implementation definition. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The Entity Type Manager. + * XXX: Ideally, this would reference an interface; however, such does not + * exist. * - * @throws \Drupal\facets\Exception\InvalidProcessorException + * @var \Drupal\islandora\IslandoraUtils */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) { - parent::__construct($configuration, $plugin_id, $plugin_definition); - $this->entityTypeManager = $entity_type_manager; - $this->storage = $entity_type_manager->getStorage('embargo'); - } + protected IslandoraUtils $islandoraUtils; /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { - return new static( - $configuration, - $plugin_id, - $plugin_definition, - $container->get('entity_type.manager') - ); + $instance = new static($configuration, $plugin_id, $plugin_definition); + + $instance->storage = $container->get('entity_type.manager')->get('embargo'); + $instance->islandoraUtils = $container->get('islandora.utils'); + + return $instance; } /** * {@inheritdoc} */ - public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) { - $properties = []; - - if ($datasource) { - $definition = [ - 'label' => $this->t('Embargo Status'), - 'description' => $this->t('Field to pass embargo status to solr index.'), - 'type' => 'string', + public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array { + if (!$datasource || !in_array($datasource->getEntityTypeId(), ['file', 'media', 'node'])) { + return []; + } + + return [ + 'embargo_info' => new EmbargoInfoProperty([ + 'label' => $this->t('Embargo info'), + 'description' => $this->t('Aggregated embargo info'), 'processor_id' => $this->getPluginId(), - 'is_list' => TRUE, - ]; - $properties['embargo_status'] = new CustomValueProperty($definition); + 'is_list' => FALSE, + 'computed' => FALSE, + ]), + ]; + } + + protected function getNodes(ItemInterface $item) : iterable { + $original = $item->getOriginalObject(); + + if ($original instanceof NodeInterface) { + yield $original; + } + elseif ($original instanceof MediaInterface) { + yield $this->islandoraUtils->getParentNode($original); } + elseif ($original instanceof FileInterface) { + foreach ($this->islandoraUtils->getReferencingMedia($original->id()) as $media) { + yield $this->islandoraUtils->getParentNode($media); + } + } + } - return $properties; + protected function getEmbargoes(ItemInterface $item) : iterable { + foreach ($this->getNodes($item) as $node) { + foreach ($this->storage->getApplicableEmbargoes($node) as $embargo) { + yield $embargo; + } + } } /** * {@inheritdoc} */ public function addFieldValues(ItemInterface $item) { - $embargo_expiration_type = $embargo_type = []; - - // Get node ID. - $item_id = $item->getId(); - $nodeId = preg_replace('/^entity:node\/(\d+):en$/', '$1', $item_id); - - // Load node based on ID, and get embargo. - $node = $this->entityTypeManager->getStorage('node')->load($nodeId); - $storages = $this->storage->getApplicableEmbargoes($node); - - if (empty($storages)) { + $info = [ + 'total_count' => 0, + 'indefinite_count' => 0, + 'scheduled_timestamps' => [], + 'exempt_users' => [], + 'exempt_ip_ranges' => [], + ]; + + $source_type_id = $item->getDatasource()->getEntityTypeId(); + if (!in_array($source_type_id, ['file', 'media', 'node'])) { return; } // Get Embargo details and prepare to pass it to index field. - foreach ($storages as $embargo) { - $allowed_expiration_types = EmbargoInterface::EXPIRATION_TYPES; - $allowed_embargo_type = EmbargoInterface::EMBARGO_TYPES; - // Get Embargo Type. - $embargo_type[] = 'embargo-type-' . $allowed_embargo_type[$embargo->getEmbargoType()]; - - // Get Expiration Type. - $embargo_expiration_type[] = 'embargo-expiration-type-' . $allowed_expiration_types[$embargo->getExpirationType()]; + foreach ($this->getEmbargoes($item) as $embargo) { + if ($embargo->getEmbargoType() === EmbargoInterface::EMBARGO_TYPE_FILE && $source_type_id === 'node') { + continue; + } + + $info['total_count']++; + if ($embargo->getExpirationType() === EmbargoInterface::EXPIRATION_TYPE_INDEFINITE) { + $info['indefinite_count']++; + } + else { + $info['scheduled_timestamps'][] = $embargo->getExpirationDate()->getTimestamp(); + } + + $info['exempt_users'] = array_merge( + $info['exempt_users'], + array_map(function (UserInterface $user) { + return $user->id(); + }, $embargo->getExemptUsers()), + ); + if ($range_id = $embargo->getExemptIps()?->id()) { + $info['exempt_ip_ranges'][] = $range_id; + } } - $embargo_status = $combinedString = implode(' ', $embargo_type) . ' ' . implode(' ', $embargo_expiration_type); + foreach (['scheduled_timestamps', 'exempt_users', 'exempt_ip_ranges'] as $key) { + $info[$key] = array_unique($info[$key]); + } $fields = $this->getFieldsHelper() - ->filterForPropertyPath($item->getFields(), $item->getDatasourceId(), 'embargo_status'); + ->filterForPropertyPath($item->getFields(), $item->getDatasourceId(), 'embargo_info'); foreach ($fields as $field) { - $field->addValue($embargo_status); + $field->addValue($info); } } From 5854a936964dd6e63b1d8803f250113531a91097 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 28 Feb 2024 12:01:09 -0400 Subject: [PATCH 07/82] Theoretical query filtering. ... most likely non-functional, but is fiiine... for now. :P Just wanting to get committed so it's not lost. --- .../processor/EmbargoInfoProperty.php | 6 +- .../search_api/processor/EmbargoProcessor.php | 111 ++++++++++++++++-- 2 files changed, 102 insertions(+), 15 deletions(-) diff --git a/src/Plugin/search_api/processor/EmbargoInfoProperty.php b/src/Plugin/search_api/processor/EmbargoInfoProperty.php index 41daca5..737cd51 100644 --- a/src/Plugin/search_api/processor/EmbargoInfoProperty.php +++ b/src/Plugin/search_api/processor/EmbargoInfoProperty.php @@ -17,7 +17,7 @@ class EmbargoInfoProperty extends ComplexDataDefinitionBase implements Processor /** * {@inheritDoc} */ - public function getPropertyDefinitions() { + public function getPropertyDefinitions() : array { if (!isset($this->propertyDefinitions)) { $this->propertyDefinitions = [ 'total_count' => new ProcessorProperty([ @@ -76,14 +76,14 @@ public function getProcessorId() { /** * {@inheritDoc} */ - public function isHidden() { + public function isHidden() : bool { return !empty($this->definition['hidden']); } /** * {@inheritdoc} */ - public function isList() { + public function isList() : bool { return (bool) ($this->definition['is_list'] ?? parent::isList()); } diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index ac7c536..ca5f87b 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -2,9 +2,10 @@ namespace Drupal\embargo\Plugin\search_api\processor; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\Core\Session\AccountProxyInterface; use Drupal\embargo\EmbargoInterface; -use Drupal\embargo\EmbargoStorageInterface; use Drupal\embargo\Plugin\search_api\processor\Property\EmbargoInfoProperty; use Drupal\file\FileInterface; use Drupal\islandora\IslandoraUtils; @@ -13,8 +14,10 @@ use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; +use Drupal\search_api\Query\QueryInterface; use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * A search_api_solr processor to add embargo related info. @@ -25,6 +28,7 @@ * description = @Translation("Processor to add information to the index related to Embargo."), * stages = { * "add_properties" = 0, + * "preprocess_query" = 10, * }, * locked = false, * hidden = false, @@ -32,12 +36,14 @@ */ class EmbargoProcessor extends ProcessorPluginBase implements ContainerFactoryPluginInterface { + const ENTITY_TYPES = ['file', 'media', 'node']; + /** - * Embargo entity storage. + * The entity type manager service. * - * @var \Drupal\embargo\EmbargoStorageInterface + * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - protected EmbargoStorageInterface $storage; + protected EntityTypeManagerInterface $entityTypeManager; /** * Islandora utils helper service. @@ -49,14 +55,20 @@ class EmbargoProcessor extends ProcessorPluginBase implements ContainerFactoryPl */ protected IslandoraUtils $islandoraUtils; + protected RequestStack $requestStack; + + protected AccountProxyInterface $currentUser; + /** * {@inheritdoc} */ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { $instance = new static($configuration, $plugin_id, $plugin_definition); - $instance->storage = $container->get('entity_type.manager')->get('embargo'); + $instance->requestStack = $container->get('request_stack'); + $instance->entityTypeManager = $container->get('entity_type.manager'); $instance->islandoraUtils = $container->get('islandora.utils'); + $instance->currentUser = $container->get('current_user'); return $instance; } @@ -65,7 +77,7 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array { - if (!$datasource || !in_array($datasource->getEntityTypeId(), ['file', 'media', 'node'])) { + if (!$datasource || !in_array($datasource->getEntityTypeId(), static::ENTITY_TYPES)) { return []; } @@ -80,6 +92,20 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : ]; } + /** + * Get the node(s) associated with the given index item. + * + * @param \Drupal\search_api\Item\ItemInterface $item + * The index item to consider. + * + * @return iterable + * A generated sequence of nodes. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\TypedData\Exception\MissingDataException + * @throws \Drupal\search_api\SearchApiException + */ protected function getNodes(ItemInterface $item) : iterable { $original = $item->getOriginalObject(); @@ -96,9 +122,23 @@ protected function getNodes(ItemInterface $item) : iterable { } } + /** + * Get the embargo(es) associated with the given index item. + * + * @param \Drupal\search_api\Item\ItemInterface $item + * The index item to consider. + * + * @return iterable + * A generated sequence of applicable embargoes. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Core\TypedData\Exception\MissingDataException + * @throws \Drupal\search_api\SearchApiException + */ protected function getEmbargoes(ItemInterface $item) : iterable { foreach ($this->getNodes($item) as $node) { - foreach ($this->storage->getApplicableEmbargoes($node) as $embargo) { + foreach ($this->entityTypeManager->getStorage('embargo')->getApplicableEmbargoes($node) as $embargo) { yield $embargo; } } @@ -117,7 +157,7 @@ public function addFieldValues(ItemInterface $item) { ]; $source_type_id = $item->getDatasource()->getEntityTypeId(); - if (!in_array($source_type_id, ['file', 'media', 'node'])) { + if (!in_array($source_type_id, static::ENTITY_TYPES)) { return; } @@ -149,12 +189,59 @@ public function addFieldValues(ItemInterface $item) { foreach (['scheduled_timestamps', 'exempt_users', 'exempt_ip_ranges'] as $key) { $info[$key] = array_unique($info[$key]); } +// +// $fields = $this->getFieldsHelper() +// ->filterForPropertyPath($item->getFields(), $item->getDatasourceId(), 'embargo_info'); +// foreach ($fields as $field) { +// $field->addValue($info); +// } + $this->ensureField($item->getDatasourceId(), 'embargo_info')->addValue($info); + } - $fields = $this->getFieldsHelper() - ->filterForPropertyPath($item->getFields(), $item->getDatasourceId(), 'embargo_info'); - foreach ($fields as $field) { - $field->addValue($info); + /** + * {@inheritDoc} + */ + public function preprocessSearchQuery(QueryInterface $query) : void { + $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; } + + foreach ($applicable_datasources as $datasource) { + //$datasource->get + } + + $fields = $query->getIndex()->getFields(); + $or_group = $query->createConditionGroup('OR'); + + // No embargo. + $or_group->addCondition('embargo_info:total_count', 0); + + // Embargo durations. + // No indefinite embargo. + $or_group->addCondition('embargo_info:indefinite_count', 0); + // No scheduled embargo in the future. + // XXX: Might not quite work? If there's a single scheduled embargo lesser, would it open it? + $or_group->addCondition('embargo_info:scheduled_timestamps', [ + 0 => time() + 1, + 1 => PHP_INT_MAX, + ], 'NOT BETWEEN'); + + if (!$this->currentUser->isAnonymous()) { + $or_group->addCondition('embargo_info:exempt_users', $this->currentUser->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('embargo_info:exempt_ip_ranges'); + } + + $query->addConditionGroup($or_group); } } From bc7ee8c35b99dbb5bea0f65926b127cda4ba7c8c Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 28 Feb 2024 12:08:55 -0400 Subject: [PATCH 08/82] Move to the appropriate directory for the namespace. --- .../search_api/processor/{ => Property}/EmbargoInfoProperty.php | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Plugin/search_api/processor/{ => Property}/EmbargoInfoProperty.php (100%) diff --git a/src/Plugin/search_api/processor/EmbargoInfoProperty.php b/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php similarity index 100% rename from src/Plugin/search_api/processor/EmbargoInfoProperty.php rename to src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php From 8c485e75821f05366c7a64fac43c4eb5b20c607e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 29 Feb 2024 16:04:22 -0400 Subject: [PATCH 09/82] Largely functional. --- .../search_api/processor/EmbargoProcessor.php | 173 ++++++++++-------- .../Property/EmbargoInfoProperty.php | 18 +- 2 files changed, 105 insertions(+), 86 deletions(-) diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index ca5f87b..fc27d54 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -5,30 +5,30 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\TypedData\ComplexDataDefinitionInterface; use Drupal\embargo\EmbargoInterface; use Drupal\embargo\Plugin\search_api\processor\Property\EmbargoInfoProperty; -use Drupal\file\FileInterface; use Drupal\islandora\IslandoraUtils; -use Drupal\media\MediaInterface; -use Drupal\node\NodeInterface; use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; use Drupal\search_api\Query\QueryInterface; +use Drupal\search_api\SearchApiException; use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; /** - * A search_api_solr processor to add embargo related info. + * A search_api processor to add embargo related info. * * @SearchApiProcessor( * id = "embargo_processor", - * label = @Translation("Embargo Processor"), - * description = @Translation("Processor to add information to the index related to Embargo."), + * label = @Translation("Embargo access"), + * description = @Translation("Add information regarding embargo access constraints."), * stages = { - * "add_properties" = 0, - * "preprocess_query" = 10, + * "add_properties" = 20, + * "pre_index_save" = 20, + * "preprocess_query" = 20, * }, * locked = false, * hidden = false, @@ -55,8 +55,18 @@ class EmbargoProcessor extends ProcessorPluginBase implements ContainerFactoryPl */ protected IslandoraUtils $islandoraUtils; + /** + * The request stack. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ protected RequestStack $requestStack; + /** + * The currently logged-in user. + * + * @var \Drupal\Core\Session\AccountProxyInterface + */ protected AccountProxyInterface $currentUser; /** @@ -77,7 +87,7 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array { - if (!$datasource || !in_array($datasource->getEntityTypeId(), static::ENTITY_TYPES)) { + if ($datasource !== NULL) { return []; } @@ -86,60 +96,49 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : 'label' => $this->t('Embargo info'), 'description' => $this->t('Aggregated embargo info'), 'processor_id' => $this->getPluginId(), - 'is_list' => FALSE, - 'computed' => FALSE, + 'is_list' => TRUE, + 'computed' => TRUE, ]), ]; } /** - * Get the node(s) associated with the given index item. + * Get the embargo(es) associated with the given index item. * * @param \Drupal\search_api\Item\ItemInterface $item * The index item to consider. * * @return iterable - * A generated sequence of nodes. + * A generated sequence of applicable embargoes. * * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException * @throws \Drupal\Core\TypedData\Exception\MissingDataException * @throws \Drupal\search_api\SearchApiException */ - protected function getNodes(ItemInterface $item) : iterable { - $original = $item->getOriginalObject(); - - if ($original instanceof NodeInterface) { - yield $original; - } - elseif ($original instanceof MediaInterface) { - yield $this->islandoraUtils->getParentNode($original); - } - elseif ($original instanceof FileInterface) { - foreach ($this->islandoraUtils->getReferencingMedia($original->id()) as $media) { - yield $this->islandoraUtils->getParentNode($media); + protected function getEmbargoes(ItemInterface $item) : iterable { + try { + foreach ($this->entityTypeManager->getStorage('embargo') + ->getApplicableEmbargoes($item->getOriginalObject()->getValue()) as $embargo) { + yield $embargo; } } + catch (SearchApiException) { + // No-op; object probably did not exist? + } } /** - * Get the embargo(es) associated with the given index item. - * - * @param \Drupal\search_api\Item\ItemInterface $item - * The index item to consider. - * - * @return iterable - * A generated sequence of applicable embargoes. - * - * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException - * @throws \Drupal\Core\TypedData\Exception\MissingDataException - * @throws \Drupal\search_api\SearchApiException + * {@inheritDoc} */ - protected function getEmbargoes(ItemInterface $item) : iterable { - foreach ($this->getNodes($item) as $node) { - foreach ($this->entityTypeManager->getStorage('embargo')->getApplicableEmbargoes($node) as $embargo) { - yield $embargo; + public function preIndexSave() { + parent::preIndexSave(); + + foreach ($this->getPropertyDefinitions() as $base => $def) { + if ($def instanceof ComplexDataDefinitionInterface) { + foreach (array_keys($def->getPropertyDefinitions()) as $name) { + $this->ensureField(NULL, "{$base}:{$name}"); + } } } } @@ -147,7 +146,12 @@ protected function getEmbargoes(ItemInterface $item) : iterable { /** * {@inheritdoc} */ - public function addFieldValues(ItemInterface $item) { + public function addFieldValues(ItemInterface $item) : void { + $source_type_id = $item->getDatasource()->getEntityTypeId(); + if (!in_array($source_type_id, static::ENTITY_TYPES)) { + return; + } + $info = [ 'total_count' => 0, 'indefinite_count' => 0, @@ -156,11 +160,6 @@ public function addFieldValues(ItemInterface $item) { 'exempt_ip_ranges' => [], ]; - $source_type_id = $item->getDatasource()->getEntityTypeId(); - if (!in_array($source_type_id, static::ENTITY_TYPES)) { - return; - } - // Get Embargo details and prepare to pass it to index field. foreach ($this->getEmbargoes($item) as $embargo) { if ($embargo->getEmbargoType() === EmbargoInterface::EMBARGO_TYPE_FILE && $source_type_id === 'node') { @@ -189,59 +188,79 @@ public function addFieldValues(ItemInterface $item) { foreach (['scheduled_timestamps', 'exempt_users', 'exempt_ip_ranges'] as $key) { $info[$key] = array_unique($info[$key]); } -// -// $fields = $this->getFieldsHelper() -// ->filterForPropertyPath($item->getFields(), $item->getDatasourceId(), 'embargo_info'); -// foreach ($fields as $field) { -// $field->addValue($info); -// } - $this->ensureField($item->getDatasourceId(), 'embargo_info')->addValue($info); + + foreach ($info as $key => $val) { + if ($field = $this->findField(NULL, "embargo_info:{$key}")) { + $item_field = $item->getField($field->getFieldIdentifier(), FALSE); + foreach ((is_array($val) ? $val : [$val]) as $value) { + $item_field->addValue($value); + } + } + } } /** * {@inheritDoc} */ public function preprocessSearchQuery(QueryInterface $query) : void { + 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) { + $applicable_datasources = array_filter($datasources, function(DatasourceInterface $datasource) { return in_array($datasource->getEntityTypeId(), static::ENTITY_TYPES); }); if (empty($applicable_datasources)) { return; } - foreach ($applicable_datasources as $datasource) { - //$datasource->get - } - - $fields = $query->getIndex()->getFields(); - $or_group = $query->createConditionGroup('OR'); + $or_group = $query->createConditionGroup('OR', [ + 'embargo_processor', + 'embargo_access', + ]); // No embargo. - $or_group->addCondition('embargo_info:total_count', 0); + if ($field = $this->findField(NULL, 'embargo_info:total_count')) { + $or_group->addCondition($field->getFieldIdentifier(), 0); + } // Embargo durations. // No indefinite embargo. - $or_group->addCondition('embargo_info:indefinite_count', 0); - // No scheduled embargo in the future. - // XXX: Might not quite work? If there's a single scheduled embargo lesser, would it open it? - $or_group->addCondition('embargo_info:scheduled_timestamps', [ - 0 => time() + 1, - 1 => PHP_INT_MAX, - ], 'NOT BETWEEN'); - - if (!$this->currentUser->isAnonymous()) { - $or_group->addCondition('embargo_info:exempt_users', $this->currentUser->id()); + if ($indefinite_field = $this->findField(NULL, 'embargo_info:indefinite_count')) { + $scheduled_group = $query->createConditionGroup(tags: [ + 'embargo_scheduled', + ]); + $scheduled_group->addCondition($indefinite_field->getFieldIdentifier(), 0); + + // No scheduled embargo in the future. + // XXX: Might not quite work? If there's a single scheduled embargo lesser, would it open it? + if ($scheduled_field = $this->findField(NULL, 'embargo_info:scheduled_timestamps')) { + $scheduled_group->addCondition($scheduled_field->getFieldIdentifier(), [ + 0 => time() + 1, + 1 => PHP_INT_MAX, + ], 'NOT BETWEEN'); + } + $or_group->addConditionGroup($scheduled_group); + } + + if (!$this->currentUser->isAnonymous() && ($field = $this->findField(NULL, 'embargo_info:exempt_users'))) { + $or_group->addCondition($field->getFieldIdentifier(), $this->currentUser->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('embargo_info:exempt_ip_ranges'); + if ($field = $this->findField(NULL, 'embargo_info:exempt_ip_ranges')) { + /** @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->addConditionGroup($or_group); + if (count($or_group->getConditions()) > 0) { + $query->addConditionGroup($or_group); + } } } diff --git a/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php b/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php index 737cd51..802aafe 100644 --- a/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php +++ b/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php @@ -23,34 +23,34 @@ public function getPropertyDefinitions() : array { 'total_count' => new ProcessorProperty([ 'label' => $this->t('Applicable embargo count'), 'description' => $this->t('Total number of applicable embargoes.'), - 'type' => 'int', + 'type' => 'integer', 'processor_id' => $this->getProcessorId(), 'is_list' => FALSE, - 'computed' => FALSE, + 'computed' => TRUE, ]), 'indefinite_count' => new ProcessorProperty([ 'label' => $this->t('Count of indefinite embargoes'), 'description' => $this->t('Total number of indefinite embargoes.'), - 'type' => 'int', + 'type' => 'integer', 'processor_id' => $this->getProcessorId(), 'is_list' => FALSE, - 'computed' => FALSE, + 'computed' => TRUE, ]), 'scheduled_timestamps' => new ProcessorProperty([ 'label' => $this->t('Schedule embargo expiry timestamps'), 'description' => $this->t('Unix timestamps when the embargoes expire.'), - 'type' => 'int', + 'type' => 'integer', 'processor_id' => $this->getProcessorId(), 'is_list' => TRUE, - 'computed' => FALSE, + 'computed' => TRUE, ]), 'exempt_users' => new ProcessorProperty([ 'label' => $this->t('IDs of users exempt from embargoes'), 'description' => '', - 'type' => 'int', + 'type' => 'integer', 'processor_id' => $this->getProcessorId(), 'is_list' => TRUE, - 'computed' => FALSE, + 'computed' => TRUE, ]), 'exempt_ip_ranges' => new ProcessorProperty([ 'label' => $this->t('IP range entity IDs'), @@ -58,7 +58,7 @@ public function getPropertyDefinitions() : array { 'type' => 'string', 'processor_id' => $this->getProcessorId(), 'is_list' => TRUE, - 'computed' => FALSE, + 'computed' => TRUE, ]), ]; } From 0ac3d69b4c6aebd66dcc49800939402c165c9b6e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 29 Feb 2024 16:06:37 -0400 Subject: [PATCH 10/82] Misc coding standards. --- .../src/Plugin/migrate/source/Entity.php | 8 +++----- tests/src/Kernel/EmbargoKernelTestBase.php | 2 +- tests/src/Kernel/IpRangeEmbargoTest.php | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) 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..864c45e 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; /** 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/IpRangeEmbargoTest.php b/tests/src/Kernel/IpRangeEmbargoTest.php index 9b29a0d..60ef26a 100644 --- a/tests/src/Kernel/IpRangeEmbargoTest.php +++ b/tests/src/Kernel/IpRangeEmbargoTest.php @@ -2,9 +2,9 @@ namespace Drupal\Tests\embargo\Kernel; -use Drupal\node\NodeInterface; use Drupal\embargo\EmbargoInterface; use Drupal\embargo\IpRangeInterface; +use Drupal\node\NodeInterface; /** * Test IpRange embargo. From debe94737b97faaac578fc5aa482941932e337e3 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 29 Feb 2024 16:07:45 -0400 Subject: [PATCH 11/82] Coding standards. --- src/Plugin/search_api/processor/EmbargoProcessor.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index fc27d54..325778b 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -209,7 +209,7 @@ public function preprocessSearchQuery(QueryInterface $query) : void { $datasources = $query->getIndex()->getDatasources(); /** @var \Drupal\search_api\Datasource\DatasourceInterface[] $applicable_datasources */ - $applicable_datasources = array_filter($datasources, function(DatasourceInterface $datasource) { + $applicable_datasources = array_filter($datasources, function (DatasourceInterface $datasource) { return in_array($datasource->getEntityTypeId(), static::ENTITY_TYPES); }); if (empty($applicable_datasources)) { @@ -235,7 +235,8 @@ public function preprocessSearchQuery(QueryInterface $query) : void { $scheduled_group->addCondition($indefinite_field->getFieldIdentifier(), 0); // No scheduled embargo in the future. - // XXX: Might not quite work? If there's a single scheduled embargo lesser, would it open it? + // XXX: Might not quite work? If there's a single scheduled embargo + // lesser, would it open it? if ($scheduled_field = $this->findField(NULL, 'embargo_info:scheduled_timestamps')) { $scheduled_group->addCondition($scheduled_field->getFieldIdentifier(), [ 0 => time() + 1, From b5f9ffa59ebcbed4c6cfe11bbae9bff46d724a29 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 29 Feb 2024 16:10:42 -0400 Subject: [PATCH 12/82] Fix test erroring. --- tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php index c30857f..b053fd8 100644 --- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php +++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php @@ -13,6 +13,13 @@ class EmbargoAccessQueryTaggingAlterTest extends EmbargoKernelTestBase { use DatabaseQueryTestTraits; + /** + * Test embargo instance. + * + * @var \Drupal\embargo\EmbargoInterface + */ + protected EmbargoInterface $embargo; + /** * {@inheritdoc} */ From 69a2df33814b95a925bf9747ea5f622e7c1fba22 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Fri, 1 Mar 2024 18:25:01 -0400 Subject: [PATCH 13/82] Rework to differently relate the embargoes to the affected entities. --- embargo.module | 34 +++ src/EmbargoItemList.php | 44 ++++ .../search_api/processor/EmbargoProcessor.php | 208 ++++++------------ .../Property/EmbargoInfoProperty.php | 90 -------- 4 files changed, 140 insertions(+), 236 deletions(-) create mode 100644 src/EmbargoItemList.php delete mode 100644 src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php diff --git a/embargo.module b/embargo.module index c5b35dc..9abeb0b 100644 --- a/embargo.module +++ b/embargo.module @@ -7,7 +7,12 @@ use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Field\BaseFieldDefinition; +use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\embargo\EmbargoInterface; +use Drupal\embargo\EmbargoItemList; use Drupal\embargo\EmbargoStorage; /** @@ -97,3 +102,32 @@ function embargo_theme($existing, $type, $theme, $path) { ], ]; } + +/** + * Implements hook_entity_base_field_info(). + */ +function embargo_entity_base_field_info(EntityTypeInterface $entity_type) { + if (!in_array($entity_type->id(), ['file', 'media', 'node'])) { + return []; + } + + $fields = []; + + $fields['embargo'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Embargoes')) + ->setDescription(t('Embargoes affecting this item.')) + ->setTranslatable(FALSE) + ->setRevisionable(FALSE) + ->setRequired(FALSE) + ->setDisplayConfigurable('view', FALSE) + ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) + ->setComputed(TRUE) + ->setClass(EmbargoItemList::class) + ->setSetting('target_type', 'embargo') + ->setSetting('embargo_types', match($entity_type->id()) { + 'file', 'media' => [EmbargoInterface::EMBARGO_TYPE_NODE, EmbargoInterface::EMBARGO_TYPE_FILE], + 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE], + }); + + return $fields; +} diff --git a/src/EmbargoItemList.php b/src/EmbargoItemList.php new file mode 100644 index 0000000..c2579d9 --- /dev/null +++ b/src/EmbargoItemList.php @@ -0,0 +1,44 @@ +getEntity(); + /** @var \Drupal\embargo\EmbargoStorageInterface $embargo_storage */ + $embargo_storage = $this->getEntityTypeManager()->getStorage('embargo'); + $this->setValue(array_filter($embargo_storage->getApplicableEmbargoes($entity), function (EmbargoInterface $embargo) { + return in_array($embargo->getEmbargoType(), $this->getSetting('embargo_types')); + })); + } + + /** + * Helper; get the entity type manager service. + * + * XXX: Dependency injection does not presently appear to be possible in typed + * data. + * + * @see https://www.drupal.org/node/2053415 + * @see https://www.drupal.org/project/drupal/issues/3294266 + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager service. + */ + protected function getEntityTypeManager() : EntityTypeManagerInterface { + return \Drupal::entityTypeManager(); + } + +} diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 325778b..fb6eefa 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -2,19 +2,15 @@ namespace Drupal\embargo\Plugin\search_api\processor; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; -use Drupal\Core\TypedData\ComplexDataDefinitionInterface; use Drupal\embargo\EmbargoInterface; -use Drupal\embargo\Plugin\search_api\processor\Property\EmbargoInfoProperty; -use Drupal\islandora\IslandoraUtils; use Drupal\search_api\Datasource\DatasourceInterface; -use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; +use Drupal\search_api\Query\ConditionGroupInterface; use Drupal\search_api\Query\QueryInterface; -use Drupal\search_api\SearchApiException; -use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -24,9 +20,9 @@ * @SearchApiProcessor( * id = "embargo_processor", * label = @Translation("Embargo access"), - * description = @Translation("Add information regarding embargo access constraints."), + * description = @Translation("Add information regarding embargo access + * constraints."), * stages = { - * "add_properties" = 20, * "pre_index_save" = 20, * "preprocess_query" = 20, * }, @@ -45,16 +41,6 @@ class EmbargoProcessor extends ProcessorPluginBase implements ContainerFactoryPl */ protected EntityTypeManagerInterface $entityTypeManager; - /** - * Islandora utils helper service. - * - * XXX: Ideally, this would reference an interface; however, such does not - * exist. - * - * @var \Drupal\islandora\IslandoraUtils - */ - protected IslandoraUtils $islandoraUtils; - /** * The request stack. * @@ -69,6 +55,13 @@ class EmbargoProcessor extends ProcessorPluginBase implements ContainerFactoryPl */ protected AccountProxyInterface $currentUser; + /** + * Drupal's time service. + * + * @var \Drupal\Component\Datetime\TimeInterface + */ + protected TimeInterface $time; + /** * {@inheritdoc} */ @@ -77,125 +70,29 @@ public static function create(ContainerInterface $container, array $configuratio $instance->requestStack = $container->get('request_stack'); $instance->entityTypeManager = $container->get('entity_type.manager'); - $instance->islandoraUtils = $container->get('islandora.utils'); $instance->currentUser = $container->get('current_user'); + $instance->time = $container->get('datetime.time'); return $instance; } - /** - * {@inheritdoc} - */ - public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array { - if ($datasource !== NULL) { - return []; - } - - return [ - 'embargo_info' => new EmbargoInfoProperty([ - 'label' => $this->t('Embargo info'), - 'description' => $this->t('Aggregated embargo info'), - 'processor_id' => $this->getPluginId(), - 'is_list' => TRUE, - 'computed' => TRUE, - ]), - ]; - } - - /** - * Get the embargo(es) associated with the given index item. - * - * @param \Drupal\search_api\Item\ItemInterface $item - * The index item to consider. - * - * @return iterable - * A generated sequence of applicable embargoes. - * - * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException - * @throws \Drupal\Core\TypedData\Exception\MissingDataException - * @throws \Drupal\search_api\SearchApiException - */ - protected function getEmbargoes(ItemInterface $item) : iterable { - try { - foreach ($this->entityTypeManager->getStorage('embargo') - ->getApplicableEmbargoes($item->getOriginalObject()->getValue()) as $embargo) { - yield $embargo; - } - } - catch (SearchApiException) { - // No-op; object probably did not exist? - } - } - /** * {@inheritDoc} */ public function preIndexSave() { parent::preIndexSave(); - foreach ($this->getPropertyDefinitions() as $base => $def) { - if ($def instanceof ComplexDataDefinitionInterface) { - foreach (array_keys($def->getPropertyDefinitions()) as $name) { - $this->ensureField(NULL, "{$base}:{$name}"); - } - } - } - } - - /** - * {@inheritdoc} - */ - public function addFieldValues(ItemInterface $item) : void { - $source_type_id = $item->getDatasource()->getEntityTypeId(); - if (!in_array($source_type_id, static::ENTITY_TYPES)) { - return; - } - - $info = [ - 'total_count' => 0, - 'indefinite_count' => 0, - 'scheduled_timestamps' => [], - 'exempt_users' => [], - 'exempt_ip_ranges' => [], - ]; - - // Get Embargo details and prepare to pass it to index field. - foreach ($this->getEmbargoes($item) as $embargo) { - if ($embargo->getEmbargoType() === EmbargoInterface::EMBARGO_TYPE_FILE && $source_type_id === 'node') { + foreach ($this->index->getDatasources() as $datasource_id => $datasource) { + if (!in_array($datasource->getEntityTypeId(), static::ENTITY_TYPES)) { continue; } - $info['total_count']++; - if ($embargo->getExpirationType() === EmbargoInterface::EXPIRATION_TYPE_INDEFINITE) { - $info['indefinite_count']++; - } - else { - $info['scheduled_timestamps'][] = $embargo->getExpirationDate()->getTimestamp(); - } - - $info['exempt_users'] = array_merge( - $info['exempt_users'], - array_map(function (UserInterface $user) { - return $user->id(); - }, $embargo->getExemptUsers()), - ); - if ($range_id = $embargo->getExemptIps()?->id()) { - $info['exempt_ip_ranges'][] = $range_id; - } - } - - foreach (['scheduled_timestamps', 'exempt_users', 'exempt_ip_ranges'] as $key) { - $info[$key] = array_unique($info[$key]); - } - - foreach ($info as $key => $val) { - if ($field = $this->findField(NULL, "embargo_info:{$key}")) { - $item_field = $item->getField($field->getFieldIdentifier(), FALSE); - foreach ((is_array($val) ? $val : [$val]) as $value) { - $item_field->addValue($value); - } - } + $this->ensureField($datasource_id, 'embargo:entity:id', 'integer'); + $this->ensureField($datasource_id, 'embargo:entity:embargo_type', 'integer'); + $this->ensureField($datasource_id, 'embargo:entity:expiration_date', 'date'); + $this->ensureField($datasource_id, 'embargo:entity:expiration_type', 'integer'); + $this->ensureField($datasource_id, 'embargo:entity:exempt_ips:entity:id', 'integer'); + $this->ensureField($datasource_id, 'embargo:entity:exempt_users:entity:uid', 'integer'); } } @@ -216,41 +113,62 @@ public function preprocessSearchQuery(QueryInterface $query) : void { return; } + $and_group = $query->createAndAddConditionGroup(tags: ['embargo_processor', 'embargo_access']); + foreach (array_keys($applicable_datasources) as $datasource_id) { + if ($filter = $this->addEmbargoFilters($datasource_id, $query)) { + $and_group->addConditionGroup($filter); + } + } + } + + /** + * Add embargo filters to the given query, for the given datasource. + * + * @param string $datasource_id + * The ID of the datasource for which to add filters. + * @param \Drupal\search_api\Query\QueryInterface $query + * The query to which to add filters. + */ + protected function addEmbargoFilters(string $datasource_id, QueryInterface $query) : ?ConditionGroupInterface { $or_group = $query->createConditionGroup('OR', [ - 'embargo_processor', - 'embargo_access', + "embargo:$datasource_id", ]); // No embargo. - if ($field = $this->findField(NULL, 'embargo_info:total_count')) { - $or_group->addCondition($field->getFieldIdentifier(), 0); + if ($field = $this->findField($datasource_id, 'embargo:entity:id')) { + $or_group->addCondition($field->getFieldIdentifier(), NULL); } // Embargo durations. // No indefinite embargo. - if ($indefinite_field = $this->findField(NULL, 'embargo_info:indefinite_count')) { - $scheduled_group = $query->createConditionGroup(tags: [ - 'embargo_scheduled', - ]); - $scheduled_group->addCondition($indefinite_field->getFieldIdentifier(), 0); - - // No scheduled embargo in the future. - // XXX: Might not quite work? If there's a single scheduled embargo - // lesser, would it open it? - if ($scheduled_field = $this->findField(NULL, 'embargo_info:scheduled_timestamps')) { - $scheduled_group->addCondition($scheduled_field->getFieldIdentifier(), [ - 0 => time() + 1, - 1 => PHP_INT_MAX, + if ($expiration_type_field = $this->findField($datasource_id, 'embargo:entity:expiration_type')) { + $or_group->addCondition($expiration_type_field->getFieldIdentifier(), EmbargoInterface::EXPIRATION_TYPE_INDEFINITE, '<>'); + + // Scheduled embargo in the past and none in the future. + if ($scheduled_field = $this->findField($datasource_id, 'embargo:entity:expiration_date')) { + $schedule_group = $query->createConditionGroup(tags: ['embargo_scheduled']); + + $schedule_group->addCondition($expiration_type_field->getFieldIdentifier(), EmbargoInterface::EXPIRATION_TYPE_SCHEDULED); + // Embargo in the past. + $schedule_group->addCondition($scheduled_field->getFieldIdentifier(), date('Y-m-d', $this->time->getRequestTime()), '<='); + // No embargo in the future. + $schedule_group->addCondition($scheduled_field->getFieldIdentifier(), [ + 0 => date('Y-m-d', strtotime('+1 DAY', $this->time->getRequestTime())), + 1 => date('Y-m-d', PHP_INT_MAX), ], 'NOT BETWEEN'); + + $or_group->addConditionGroup($schedule_group); } - $or_group->addConditionGroup($scheduled_group); } - if (!$this->currentUser->isAnonymous() && ($field = $this->findField(NULL, 'embargo_info:exempt_users'))) { + if ( + !$this->currentUser->isAnonymous() && + ($field = $this->findField($datasource_id, 'embargo:entity:exempt_users:entity:uid')) + ) { $or_group->addCondition($field->getFieldIdentifier(), $this->currentUser->id()); } - if ($field = $this->findField(NULL, 'embargo_info:exempt_ip_ranges')) { + if ($field = $this->findField($datasource_id, 'embargo:entity:exempt_ips:entity:id')) { /** @var \Drupal\embargo\IpRangeStorageInterface $ip_range_storage */ $ip_range_storage = $this->entityTypeManager->getStorage('embargo_ip_range'); foreach ($ip_range_storage->getApplicableIpRanges($this->requestStack->getCurrentRequest() @@ -259,9 +177,7 @@ public function preprocessSearchQuery(QueryInterface $query) : void { } } - if (count($or_group->getConditions()) > 0) { - $query->addConditionGroup($or_group); - } + return (count($or_group->getConditions()) > 0) ? $or_group : NULL; } } diff --git a/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php b/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php deleted file mode 100644 index 802aafe..0000000 --- a/src/Plugin/search_api/processor/Property/EmbargoInfoProperty.php +++ /dev/null @@ -1,90 +0,0 @@ -propertyDefinitions)) { - $this->propertyDefinitions = [ - 'total_count' => new ProcessorProperty([ - 'label' => $this->t('Applicable embargo count'), - 'description' => $this->t('Total number of applicable embargoes.'), - 'type' => 'integer', - 'processor_id' => $this->getProcessorId(), - 'is_list' => FALSE, - 'computed' => TRUE, - ]), - 'indefinite_count' => new ProcessorProperty([ - 'label' => $this->t('Count of indefinite embargoes'), - 'description' => $this->t('Total number of indefinite embargoes.'), - 'type' => 'integer', - 'processor_id' => $this->getProcessorId(), - 'is_list' => FALSE, - 'computed' => TRUE, - ]), - 'scheduled_timestamps' => new ProcessorProperty([ - 'label' => $this->t('Schedule embargo expiry timestamps'), - 'description' => $this->t('Unix timestamps when the embargoes expire.'), - 'type' => 'integer', - 'processor_id' => $this->getProcessorId(), - 'is_list' => TRUE, - 'computed' => TRUE, - ]), - 'exempt_users' => new ProcessorProperty([ - 'label' => $this->t('IDs of users exempt from embargoes'), - 'description' => '', - 'type' => 'integer', - 'processor_id' => $this->getProcessorId(), - 'is_list' => TRUE, - 'computed' => TRUE, - ]), - 'exempt_ip_ranges' => new ProcessorProperty([ - 'label' => $this->t('IP range entity IDs'), - 'description' => '', - 'type' => 'string', - 'processor_id' => $this->getProcessorId(), - 'is_list' => TRUE, - 'computed' => TRUE, - ]), - ]; - } - - return $this->propertyDefinitions; - } - - /** - * {@inheritDoc} - */ - public function getProcessorId() { - return $this->definition['processor_id']; - } - - /** - * {@inheritDoc} - */ - public function isHidden() : bool { - return !empty($this->definition['hidden']); - } - - /** - * {@inheritdoc} - */ - public function isList() : bool { - return (bool) ($this->definition['is_list'] ?? parent::isList()); - } - -} From 883fba3273fc77768eeb5ac3872a0871053db260 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 09:39:41 -0400 Subject: [PATCH 14/82] Avoid letting scheduled embargoes through if there are indefinite. --- src/Plugin/search_api/processor/EmbargoProcessor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index fb6eefa..8f725af 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -148,6 +148,7 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer if ($scheduled_field = $this->findField($datasource_id, 'embargo:entity:expiration_date')) { $schedule_group = $query->createConditionGroup(tags: ['embargo_scheduled']); + $schedule_group->addCondition($expiration_type_field->getFieldIdentifier(), EmbargoInterface::EXPIRATION_TYPE_INDEFINITE, '<>'); $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()), '<='); From a4a8d3e2838aa0a6b32d001ecf4493f0da6822a0 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 09:41:30 -0400 Subject: [PATCH 15/82] Coding standards. --- embargo.module | 5 ++++- src/Plugin/search_api/processor/EmbargoProcessor.php | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/embargo.module b/embargo.module index 9abeb0b..ced795b 100644 --- a/embargo.module +++ b/embargo.module @@ -125,7 +125,10 @@ function embargo_entity_base_field_info(EntityTypeInterface $entity_type) { ->setClass(EmbargoItemList::class) ->setSetting('target_type', 'embargo') ->setSetting('embargo_types', match($entity_type->id()) { - 'file', 'media' => [EmbargoInterface::EMBARGO_TYPE_NODE, EmbargoInterface::EMBARGO_TYPE_FILE], + 'file', 'media' => [ + EmbargoInterface::EMBARGO_TYPE_NODE, + EmbargoInterface::EMBARGO_TYPE_FILE, + ], 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE], }); diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 8f725af..7d5aa9b 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -113,7 +113,10 @@ public function preprocessSearchQuery(QueryInterface $query) : void { return; } - $and_group = $query->createAndAddConditionGroup(tags: ['embargo_processor', 'embargo_access']); + $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); From 01b77bb754f73dc981ae0201dce769dee9f9b354 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 12:04:55 -0400 Subject: [PATCH 16/82] Attempt rolling additional tracking. ... didn't seem to work. --- embargo.services.yml | 4 ++ .../TrackingEventSubscriber.php | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 src/EventSubscriber/TrackingEventSubscriber.php diff --git a/embargo.services.yml b/embargo.services.yml index 38b4738..23659b9 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -31,3 +31,7 @@ services: - '@url_generator' tags: - { name: 'event_subscriber' } + embargo.tracking_event_subscriber: + class: Drupal\embargo\EventSubscriber\TrackingEventSubscriber + tags: + - { name: 'event_subscriber' } diff --git a/src/EventSubscriber/TrackingEventSubscriber.php b/src/EventSubscriber/TrackingEventSubscriber.php new file mode 100644 index 0000000..538c56f --- /dev/null +++ b/src/EventSubscriber/TrackingEventSubscriber.php @@ -0,0 +1,46 @@ + 'foreignRelationshipMap', + ]; + } + + /** + * Inform search_api of our computed entity_reference listings. + * + * @param \Drupal\search_api\Event\MappingForeignRelationshipsEvent $event + * The event by which to inform. + */ + public function foreignRelationshipMap(MappingForeignRelationshipsEvent $event) : void { + $mapping =& $event->getForeignRelationshipsMapping(); + $index = $event->getIndex(); + foreach ($index->getDatasources() as $id => $datasource) { + if (!in_array($datasource->getEntityTypeId(), ['file', 'media', 'node'])) { + continue; + } + + $mapping[] = [ + 'datasource' => $id, + 'entity_type' => 'embargo', + 'property_path_to_foreign_entity' => 'embargo:entity:id', + 'field_name' => 'id', + ]; + } + } + +} From a82bdbbc1db6b1d713ad4a7002c032ad29b05527 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 12:05:51 -0400 Subject: [PATCH 17/82] Roll in additional entity tracking. --- embargo.module | 76 +++++++++++++++++++ embargo.services.yml | 4 - .../TrackingEventSubscriber.php | 46 ----------- 3 files changed, 76 insertions(+), 50 deletions(-) delete mode 100644 src/EventSubscriber/TrackingEventSubscriber.php diff --git a/embargo.module b/embargo.module index ced795b..1dfe943 100644 --- a/embargo.module +++ b/embargo.module @@ -6,6 +6,7 @@ */ use Drupal\Core\Database\Query\AlterableInterface; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Field\BaseFieldDefinition; @@ -14,6 +15,7 @@ use Drupal\Core\Session\AccountInterface; use Drupal\embargo\EmbargoInterface; use Drupal\embargo\EmbargoItemList; use Drupal\embargo\EmbargoStorage; +use Drupal\islandora_hierarchical_access\LUTGeneratorInterface; /** * Implements hook_entity_type_alter(). @@ -134,3 +136,77 @@ function embargo_entity_base_field_info(EntityTypeInterface $entity_type) { return $fields; } + +/** + * Implements hook_ENTITY_TYPE_insert() for embargo entities. + */ +function embargo_embargo_insert(EntityInterface $entity) : void { + _embargo_search_api_track($entity); +} + +/** + * Implements hook_ENTITY_TYPE_update() for embargo entities. + */ +function embargo_embargo_update(EntityInterface $entity) : void { + _embargo_search_api_track($entity); +} + +/** + * Implements hook_ENTITY_TYPE_delete() for embargo entities. + */ +function embargo_embargo_delete(EntityInterface $entity) : void { + _embargo_search_api_track($entity); +} + +/** + * Helper; deal with updating indexes of related items. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The embargo instance. + */ +function _embargo_search_api_track(EntityInterface $entity) : void { + assert($entity instanceof EmbargoInterface); + if (!\Drupal::moduleHandler()->moduleExists('search_api')) { + return; + } + + // On updates, deal with the original value, in addition to the new. + if (isset($entity->original)) { + _embargo_search_api_track($entity->original); + } + + if (!($node = $entity->getEmbargoedNode())) { + // No embargoed node? + return; + } + + /** @var \Drupal\search_api\Plugin\search_api\datasource\ContentEntityTrackingManager $tracking_manager */ + $tracking_manager = \Drupal::getContainer()->get('search_api.entity_datasource.tracking_manager'); + /** @var \Drupal\search_api\Utility\TrackingHelperInterface $tracking_helper */ + $tracking_helper = \Drupal::getContainer()->get('search_api.tracking_helper'); + + $track = function (ContentEntityInterface $entity) use ($tracking_manager, $tracking_helper) { + $tracking_manager->trackEntityChange($entity); + $tracking_helper->trackReferencedEntityUpdate($entity); + }; + + $track($node); + + $results = \Drupal::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)); + + $entity_type_manager = \Drupal::entityTypeManager(); + /** @var \Drupal\media\MediaInterface $media */ + foreach ($entity_type_manager->getStorage('media')->loadMultiple($media_ids) as $media) { + $track($media); + } + /** @var \Drupal\file\FileInterface $file */ + foreach ($entity_type_manager->getStorage('file')->loadMultiple($file_ids) as $file) { + $track($file); + } + +} diff --git a/embargo.services.yml b/embargo.services.yml index 23659b9..38b4738 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -31,7 +31,3 @@ services: - '@url_generator' tags: - { name: 'event_subscriber' } - embargo.tracking_event_subscriber: - class: Drupal\embargo\EventSubscriber\TrackingEventSubscriber - tags: - - { name: 'event_subscriber' } diff --git a/src/EventSubscriber/TrackingEventSubscriber.php b/src/EventSubscriber/TrackingEventSubscriber.php deleted file mode 100644 index 538c56f..0000000 --- a/src/EventSubscriber/TrackingEventSubscriber.php +++ /dev/null @@ -1,46 +0,0 @@ - 'foreignRelationshipMap', - ]; - } - - /** - * Inform search_api of our computed entity_reference listings. - * - * @param \Drupal\search_api\Event\MappingForeignRelationshipsEvent $event - * The event by which to inform. - */ - public function foreignRelationshipMap(MappingForeignRelationshipsEvent $event) : void { - $mapping =& $event->getForeignRelationshipsMapping(); - $index = $event->getIndex(); - foreach ($index->getDatasources() as $id => $datasource) { - if (!in_array($datasource->getEntityTypeId(), ['file', 'media', 'node'])) { - continue; - } - - $mapping[] = [ - 'datasource' => $id, - 'entity_type' => 'embargo', - 'property_path_to_foreign_entity' => 'embargo:entity:id', - 'field_name' => 'id', - ]; - } - } - -} From 1c371d7db4c363aa3347def66288e787f73ddce5 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 12:26:17 -0400 Subject: [PATCH 18/82] Fix up logic, move comments around. Goof in the "OR", such that no "indefinite" embargo was sufficient (it ignored the scheduled). --- .../search_api/processor/EmbargoProcessor.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 7d5aa9b..28276cc 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -142,16 +142,14 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer $or_group->addCondition($field->getFieldIdentifier(), NULL); } - // Embargo durations. - // No indefinite embargo. + // Embargo duration/schedule. if ($expiration_type_field = $this->findField($datasource_id, 'embargo:entity:expiration_type')) { - $or_group->addCondition($expiration_type_field->getFieldIdentifier(), EmbargoInterface::EXPIRATION_TYPE_INDEFINITE, '<>'); + $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:entity:expiration_date')) { - $schedule_group = $query->createConditionGroup(tags: ['embargo_scheduled']); - - $schedule_group->addCondition($expiration_type_field->getFieldIdentifier(), EmbargoInterface::EXPIRATION_TYPE_INDEFINITE, '<>'); $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()), '<='); @@ -160,9 +158,9 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer 0 => date('Y-m-d', strtotime('+1 DAY', $this->time->getRequestTime())), 1 => date('Y-m-d', PHP_INT_MAX), ], 'NOT BETWEEN'); - - $or_group->addConditionGroup($schedule_group); } + + $or_group->addConditionGroup($schedule_group); } if ( From 15b901be72f9612441a5e7cdb4163e44c0485967 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 13:41:09 -0400 Subject: [PATCH 19/82] Don't strictly need to pass the first offset. Comment it out to document that it's intended to be the 0th, explicitly? --- embargo.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embargo.module b/embargo.module index 1dfe943..0874bf1 100644 --- a/embargo.module +++ b/embargo.module @@ -196,7 +196,7 @@ function _embargo_search_api_track(EntityInterface $entity) : void { ->fields('lut', ['mid', 'fid']) ->condition('nid', $node->id()) ->execute(); - $media_ids = array_unique($results->fetchCol(0)); + $media_ids = array_unique($results->fetchCol(/* 0 */)); $file_ids = array_unique($results->fetchCol(1)); $entity_type_manager = \Drupal::entityTypeManager(); From 12ba90ec5377e6d676fde1ce856aebdbe8051485 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 15:27:33 -0400 Subject: [PATCH 20/82] Start thinking to roll in more cache headers; however, shouldn't be necessary. --- embargo.services.yml | 4 ++ .../ViewsCacheEventSubscriber.php | 63 +++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 src/EventSubscriber/ViewsCacheEventSubscriber.php diff --git a/embargo.services.yml b/embargo.services.yml index 38b4738..0401c2c 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -31,3 +31,7 @@ services: - '@url_generator' tags: - { name: 'event_subscriber' } + embargo.search_api.pre_execute_subscriber: + class: Drupal\embargo\EventSubscriber\ViewsCacheEventSubscriber + tags: + - { name: 'event_subscriber' } diff --git a/src/EventSubscriber/ViewsCacheEventSubscriber.php b/src/EventSubscriber/ViewsCacheEventSubscriber.php new file mode 100644 index 0000000..5f8ac0c --- /dev/null +++ b/src/EventSubscriber/ViewsCacheEventSubscriber.php @@ -0,0 +1,63 @@ + 'preExecute', + //'search_api.query_pre_execute.alter_cache_metadata' => ['alterCacheMetadata'], + ]; + } + + public function preExecute(QueryPreExecuteEvent $event) { + dsm($event->getQuery()->getTags()); + if ($event->getQuery()->hasTag('alter_cache_metadata')) { + $this->alterCacheMetadata($event); + } + + $query = $event->getQuery(); + if ($query instanceof RefinableCacheableDependencyInterface) { + dsm($query->getCacheContexts()); + dsm($query->getCacheTags()); + dsm($query->getCacheMaxAge()); + } + } + + /** + * Alter cache metadata. + * + * @param \Drupal\search_api\Event\QueryPreExecuteEvent $event + * The pre-execution event. + */ + public function alterCacheMetadata(QueryPreExecuteEvent $event) : void { + $query = $event->getQuery(); + if (!($query instanceof RefinableCacheableDependencyInterface)) { + dsm('blah'); + // Cannot add metadata to it. + return; + } + if ($this->containsRelevantIndex($query)) { + //$query->add + } + } + + protected function containsRelevantIndex(QueryInterface $query) : bool { + dsm($query->getTags()); + return FALSE; + } + +} From 80d5b997f3c5ca120de5d0397be60e862a737026 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 15:28:22 -0400 Subject: [PATCH 21/82] More cache info. --- embargo.services.yml | 4 -- .../ViewsCacheEventSubscriber.php | 63 ------------------- .../search_api/processor/EmbargoProcessor.php | 17 +++-- 3 files changed, 13 insertions(+), 71 deletions(-) delete mode 100644 src/EventSubscriber/ViewsCacheEventSubscriber.php diff --git a/embargo.services.yml b/embargo.services.yml index 0401c2c..38b4738 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -31,7 +31,3 @@ services: - '@url_generator' tags: - { name: 'event_subscriber' } - embargo.search_api.pre_execute_subscriber: - class: Drupal\embargo\EventSubscriber\ViewsCacheEventSubscriber - tags: - - { name: 'event_subscriber' } diff --git a/src/EventSubscriber/ViewsCacheEventSubscriber.php b/src/EventSubscriber/ViewsCacheEventSubscriber.php deleted file mode 100644 index 5f8ac0c..0000000 --- a/src/EventSubscriber/ViewsCacheEventSubscriber.php +++ /dev/null @@ -1,63 +0,0 @@ - 'preExecute', - //'search_api.query_pre_execute.alter_cache_metadata' => ['alterCacheMetadata'], - ]; - } - - public function preExecute(QueryPreExecuteEvent $event) { - dsm($event->getQuery()->getTags()); - if ($event->getQuery()->hasTag('alter_cache_metadata')) { - $this->alterCacheMetadata($event); - } - - $query = $event->getQuery(); - if ($query instanceof RefinableCacheableDependencyInterface) { - dsm($query->getCacheContexts()); - dsm($query->getCacheTags()); - dsm($query->getCacheMaxAge()); - } - } - - /** - * Alter cache metadata. - * - * @param \Drupal\search_api\Event\QueryPreExecuteEvent $event - * The pre-execution event. - */ - public function alterCacheMetadata(QueryPreExecuteEvent $event) : void { - $query = $event->getQuery(); - if (!($query instanceof RefinableCacheableDependencyInterface)) { - dsm('blah'); - // Cannot add metadata to it. - return; - } - if ($this->containsRelevantIndex($query)) { - //$query->add - } - } - - protected function containsRelevantIndex(QueryInterface $query) : bool { - dsm($query->getTags()); - return FALSE; - } - -} diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 28276cc..e343d66 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -3,6 +3,7 @@ namespace Drupal\embargo\Plugin\search_api\processor; use Drupal\Component\Datetime\TimeInterface; +use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; @@ -100,6 +101,8 @@ public function preIndexSave() { * {@inheritDoc} */ public function preprocessSearchQuery(QueryInterface $query) : void { + assert($query instanceof RefinableCacheableDependencyInterface); + $query->addCacheContexts(['user.permissions']); if ($this->currentUser->hasPermission('bypass embargo access')) { return; } @@ -133,6 +136,7 @@ public function preprocessSearchQuery(QueryInterface $query) : void { * 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", ]); @@ -140,6 +144,7 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer // No embargo. if ($field = $this->findField($datasource_id, 'embargo:entity:id')) { $or_group->addCondition($field->getFieldIdentifier(), NULL); + $query->addCacheTags(['embargo_list']); } // Embargo duration/schedule. @@ -158,16 +163,19 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer 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() && - ($field = $this->findField($datasource_id, 'embargo:entity:exempt_users:entity:uid')) - ) { + if ($this->currentUser->isAnonymous()) { + $query->addCacheContexts(['user.roles:anonymous']); + } + elseif ($field = $this->findField($datasource_id, 'embargo:entity:exempt_users:entity:uid')) { $or_group->addCondition($field->getFieldIdentifier(), $this->currentUser->id()); + $query->addCacheContexts(['user']); } if ($field = $this->findField($datasource_id, 'embargo:entity:exempt_ips:entity:id')) { @@ -177,6 +185,7 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer ->getClientIp()) as $ipRange) { $or_group->addCondition($field->getFieldIdentifier(), $ipRange->id()); } + $query->addCacheContexts(['ip']); } return (count($or_group->getConditions()) > 0) ? $or_group : NULL; From 92b4b05293be36442290d6580b4d17f290e06f9c Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 4 Mar 2024 16:53:45 -0400 Subject: [PATCH 22/82] Additional cache tag dealio. --- src/Plugin/search_api/processor/EmbargoProcessor.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index e343d66..6e3852f 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -184,6 +184,7 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer foreach ($ip_range_storage->getApplicableIpRanges($this->requestStack->getCurrentRequest() ->getClientIp()) as $ipRange) { $or_group->addCondition($field->getFieldIdentifier(), $ipRange->id()); + $query->addCacheableDependency($ipRange); } $query->addCacheContexts(['ip']); } From 9a0bdb3d8c7376b771c88055aa6ab162a38cf09e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 5 Mar 2024 16:19:38 -0400 Subject: [PATCH 23/82] Further attempt to avoid joins. Track things as metadata on the query? --- src/Access/QueryTagger.php | 71 +++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index 0de4128..d8a0524 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -26,28 +26,28 @@ class QueryTagger { * * @var \Drupal\Core\Session\AccountProxyInterface */ - protected $user; + protected AccountProxyInterface $user; /** * The IP of the request. * * @var string */ - protected $currentIp; + protected string $currentIp; /** * Instance of a Drupal database connection. * * @var \Drupal\Core\Database\Connection */ - protected $database; + protected Connection $database; /** * The entity type manager. * * @var \Drupal\Core\Entity\EntityTypeManagerInterface */ - protected $entityTypeManager; + protected EntityTypeManagerInterface $entityTypeManager; /** * Time service. @@ -106,6 +106,8 @@ public function tagAccess(SelectInterface $query, string $type) : void { $target_aliases = []; + $tagged_table_aliases = $query->getMetaData('embargo_tagged_table_aliases') ?? []; + foreach ($query->getTables() as $info) { if ($info['table'] instanceof SelectInterface) { continue; @@ -113,7 +115,10 @@ public function tagAccess(SelectInterface $query, string $type) : void { elseif (in_array($info['table'], $tables)) { $key = (str_starts_with($info['table'], "{$type}__")) ? 'entity_id' : (substr($type, 0, 1) . "id"); $alias = $info['alias']; - $target_aliases[] = "{$alias}.{$key}"; + if (!in_array($alias, $tagged_table_aliases)) { + $tagged_table_aliases[] = $alias; + $target_aliases[] = "{$alias}.{$key}"; + } } } @@ -121,15 +126,45 @@ public function tagAccess(SelectInterface $query, string $type) : void { return; } - $existence = $this->database->select('node', 'existence_node'); - $existence->fields('existence_node', ['nid']); + $query->addMetaData('embargo_tagged_table_aliases', $tagged_table_aliases); + $existence = $query->getMetaData('embargo_tagged_existence_query'); + + if (!$existence) { + $existence = $this->database->select('node', 'existence_node'); + $existence->fields('existence_node', ['nid']); + $existence_lut_alias = $existence->leftJoin(LUTGeneratorInterface::TABLE_NAME, 'lut', '%alias.nid = existence_node.nid'); + $query->addMetaData('embargo_tagged_existence_query', $existence); + $query->addMetaData('embargo_lut_alias', $existence_lut_alias); + + $exist_or = $existence->orConditionGroup(); + + // No embargo. + $embargo = $this->database->select('embargo', 'ee'); + $embargo->fields('ee', ['embargoed_node']); + $embargo->where('existence_node.nid = ee.embargoed_node'); + $exist_or->notExists($embargo); + + // Embargoed (and allowed). + $accessible_embargoes = $this->buildAccessibleEmbargoesQuery(match($type) { + 'file', 'media' => EmbargoInterface::EMBARGO_TYPE_FILE, + 'node' => EmbargoInterface::EMBARGO_TYPE_NODE, + }); + $accessible_embargoes->where('existence_node.nid = e.embargoed_node'); + $exist_or->exists($accessible_embargoes); + + $existence->condition($exist_or); + + $query->exists($existence); + } + else { + $existence_lut_alias = $query->getMetaData('embargo_lut_alias'); + } if ($type !== 'node') { $lut_column = match($type) { 'file' => 'fid', 'media' => 'mid', }; - $existence_lut_alias = $existence->leftJoin(LUTGeneratorInterface::TABLE_NAME, 'lut', '%alias.nid = existence_node.nid'); $existence->where(strtr('!field IS NULL OR !field IN (!targets)', [ '!field' => "{$existence_lut_alias}.{$lut_column}", '!targets' => implode(', ', $target_aliases), @@ -141,26 +176,6 @@ public function tagAccess(SelectInterface $query, string $type) : void { '!targets' => implode(', ', $target_aliases), ])); } - - $exist_or = $existence->orConditionGroup(); - - // No embargo. - $embargo = $this->database->select('embargo', 'ee'); - $embargo->fields('ee', ['embargoed_node']); - $embargo->where('existence_node.nid = ee.embargoed_node'); - $exist_or->notExists($embargo); - - // Embargoed (and allowed). - $accessible_embargoes = $this->buildAccessibleEmbargoesQuery(match($type) { - 'file', 'media' => EmbargoInterface::EMBARGO_TYPE_FILE, - 'node' => EmbargoInterface::EMBARGO_TYPE_NODE, - }); - $accessible_embargoes->where('existence_node.nid = e.embargoed_node'); - $exist_or->exists($accessible_embargoes); - - $existence->condition($exist_or); - - $query->exists($existence); } /** From 8465ffe362b5bf6fbc5a48e0cee899b9f71b347a Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 6 Mar 2024 18:57:22 -0400 Subject: [PATCH 24/82] Found some issues in the test, oddly enough. See: https://github.com/discoverygarden/embargo/pull/6#pullrequestreview-1920893091 Some mixed up assertions, assuming that the ::setUp() method changed at some point mid-implementation, and wasn't carried through? And that it that the existing implementation was indeed bearing a bug. --- .../EmbargoAccessQueryTaggingAlterTest.php | 86 ++++++++++++++++--- 1 file changed, 76 insertions(+), 10 deletions(-) diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php index b053fd8..46eb041 100644 --- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php +++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php @@ -3,6 +3,9 @@ namespace Drupal\Tests\embargo\Kernel; use Drupal\embargo\EmbargoInterface; +use Drupal\file\FileInterface; +use Drupal\media\MediaInterface; +use Drupal\node\NodeInterface; use Drupal\Tests\islandora_test_support\Traits\DatabaseQueryTestTraits; /** @@ -20,6 +23,48 @@ class EmbargoAccessQueryTaggingAlterTest extends EmbargoKernelTestBase { */ 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; + /** * {@inheritdoc} */ @@ -27,11 +72,12 @@ public function setUp(): void { parent::setUp(); // 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); } /** @@ -43,6 +89,7 @@ public function testEmbargoNodeQueryAlterAccess() { $query = $this->generateNodeSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); $this->assertCount(1, $result, 'User can only view non-embargoed node.'); + $this->assertEquals([$this->unembargoedNode->id()], array_column($result, 'nid')); } /** @@ -53,7 +100,8 @@ 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'); + $this->assertCount(1, $result, 'Media of embargoed nodes cannot be viewed'); + $this->assertEquals([$this->unembargoedMedia->id()], array_column($result, 'mid')); } /** @@ -65,6 +113,7 @@ public function testNodeEmbargoReferencedFileAccessQueryAlterAccessDenied() { $query = $this->generateFileSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); $this->assertCount(1, $result, 'File of embargoed nodes cannot be viewed'); + $this->assertEquals([$this->unembargoedFile->id()], array_column($result, 'fid')); } /** @@ -80,6 +129,10 @@ public function testDeletedNodeEmbargoNodeAccessQueryAlterAccessAllowed() { $result = $query->execute()->fetchAll(); $this->assertCount(2, $result, 'Non embargoed nodes can be viewed'); + $this->assertEqualsCanonicalizing([ + $this->embargoedNode->id(), + $this->unembargoedNode->id(), + ], array_column($result, 'nid')); } /** @@ -93,8 +146,12 @@ public function testDeletedNodeEmbargoMediaAccessQueryAlterAccessAllowed() { $this->embargo->delete(); $query = $this->generateMediaSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(1, $result, + $this->assertCount(2, $result, 'Media of non embargoed nodes can be viewed'); + $this->assertEqualsCanonicalizing([ + $this->embargoedMedia->id(), + $this->unembargoedMedia->id(), + ], array_column($result, 'mid')); } /** @@ -111,6 +168,10 @@ public function testDeletedNodeEmbargoFileAccessQueryAlterAccessAllowed() { $result = $query->execute()->fetchAll(); $this->assertCount(2, $result, 'Files of non embargoed nodes can be viewed'); + $this->assertEqualsCanonicalizing([ + $this->embargoedFile->id(), + $this->unembargoedFile->id(), + ], array_column($result, 'fid')); } /** @@ -122,9 +183,10 @@ public function testPublishScheduledEmbargoAccess() { // Create an embargo scheduled to be unpublished in the future. $this->setEmbargoFutureUnpublishDate($this->embargo); - $nodeCount = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); - $this->assertCount(1, $nodeCount, + $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); + $this->assertCount(1, $result, 'Node is still embargoed.'); + $this->assertEqualsCanonicalizing([$this->unembargoedNode->id()], array_column($result, 'nid')); } /** @@ -137,9 +199,13 @@ 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, + $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); + $this->assertCount(2, $result, 'Embargo has been unpublished.'); + $this->assertEqualsCanonicalizing([ + $this->embargoedNode->id(), + $this->unembargoedNode->id(), + ], array_column($result, 'nid')); } } From c484d01f39439290b27c2eab7b276b264d506395 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 6 Mar 2024 19:58:06 -0400 Subject: [PATCH 25/82] Presently functional, need to DRY up a bit. ... DRY-ing up in progress, with the trait. --- embargo.module | 20 +-- embargo.services.yml | 7 + src/Access/QueryTagger.php | 116 ++++++++------ src/EmbargoExistenceQueryTrait.php | 78 +++++++++ ...ndoraHierarchicalAccessEventSubscriber.php | 149 ++++++++++++++++++ 5 files changed, 302 insertions(+), 68 deletions(-) create mode 100644 src/EmbargoExistenceQueryTrait.php create mode 100644 src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php diff --git a/embargo.module b/embargo.module index c5b35dc..34cb5e5 100644 --- a/embargo.module +++ b/embargo.module @@ -52,25 +52,7 @@ function embargo_file_download($uri) { 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'); -} - -/** - * Implements hook_query_TAG_alter() for `file_access` tagged queries. - */ -function embargo_query_file_access_alter(AlterableInterface $query) { - /** @var \Drupal\embargo\Access\QueryTagger $tagger */ - $tagger = \Drupal::service('embargo.query_tagger'); - $tagger->tagAccess($query, 'file'); + $tagger->tagNode($query); } /** diff --git a/embargo.services.yml b/embargo.services.yml index 38b4738..b316143 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -31,3 +31,10 @@ 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' } diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index d8a0524..6422f0d 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -87,16 +87,16 @@ 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) : void { - 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 add again. + return; } - elseif ($this->user->hasPermission('bypass embargo access')) { + if ($this->user->hasPermission('bypass embargo access')) { return; } + $type = 'node'; static::conjunctionQuery($query); @@ -127,54 +127,72 @@ public function tagAccess(SelectInterface $query, string $type) : void { } $query->addMetaData('embargo_tagged_table_aliases', $tagged_table_aliases); - $existence = $query->getMetaData('embargo_tagged_existence_query'); + $existence_query = $query->getMetaData('embargo_tagged_existence_query'); + + if (!$existence_query) { + $existence_query = $this->database->select('node', 'existence_node'); + $existence_query->fields('existence_node', ['nid']); + $query->addMetaData('embargo_tagged_existence_query', $existence_query); + + $query->exists($existence_query); + } + + $existence_query->where(strtr('!field IN (!targets)', [ + '!field' => 'existence_node.nid', + '!targets' => implode(', ', $target_aliases), + ])); - if (!$existence) { - $existence = $this->database->select('node', 'existence_node'); - $existence->fields('existence_node', ['nid']); - $existence_lut_alias = $existence->leftJoin(LUTGeneratorInterface::TABLE_NAME, 'lut', '%alias.nid = existence_node.nid'); - $query->addMetaData('embargo_tagged_existence_query', $existence); - $query->addMetaData('embargo_lut_alias', $existence_lut_alias); + if (!$query->hasTag('embargo_access')) { + $query->addTag('embargo_access'); - $exist_or = $existence->orConditionGroup(); + $embargo_alias = $existence_query->leftJoin('embargo', 'e', '%alias.embargoed_node = existence_node.nid'); + $user_alias = $existence_query->leftJoin('embargo__exempt_users', 'u', "%alias.entity_id = {$embargo_alias}.id"); + $existence_or = $existence_query->orConditionGroup(); // No embargo. - $embargo = $this->database->select('embargo', 'ee'); - $embargo->fields('ee', ['embargoed_node']); - $embargo->where('existence_node.nid = ee.embargoed_node'); - $exist_or->notExists($embargo); - - // Embargoed (and allowed). - $accessible_embargoes = $this->buildAccessibleEmbargoesQuery(match($type) { - 'file', 'media' => EmbargoInterface::EMBARGO_TYPE_FILE, - 'node' => EmbargoInterface::EMBARGO_TYPE_NODE, - }); - $accessible_embargoes->where('existence_node.nid = e.embargoed_node'); - $exist_or->exists($accessible_embargoes); - - $existence->condition($exist_or); - - $query->exists($existence); - } - else { - $existence_lut_alias = $query->getMetaData('embargo_lut_alias'); - } + // XXX: Might have to change to examine one of the fields outside the join + // condition? + $existence_or->isNull("{$embargo_alias}.embargoed_node"); + + // The user is exempt from the embargo. + $existence_or->condition("{$user_alias}.exempt_users_target_id", $this->user->id()); + + // ... 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)) { + $existence_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN'); + } - if ($type !== 'node') { - $lut_column = match($type) { - 'file' => 'fid', - 'media' => 'mid', - }; - $existence->where(strtr('!field IS NULL OR !field IN (!targets)', [ - '!field' => "{$existence_lut_alias}.{$lut_column}", - '!targets' => implode(', ', $target_aliases), - ])); - } - else { - $existence->where(strtr('!field IN (!targets)', [ - '!field' => 'existence_node.nid', - '!targets' => implode(', ', $target_aliases), - ])); + // With embargo, without exemption. + $embargo_and = $existence_or->andConditionGroup(); + + // Has an embargo of a relevant type. + $embargo_and->condition("{$embargo_alias}.embargo_type", EmbargoInterface::EMBARGO_TYPE_NODE); + + $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') + ->fields('ue', ['embargoed_node']); + $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_and + ->condition( + "{$embargo_alias}.embargoed_node", + $unexpired_embargo_subquery, + 'NOT IN', + ) + ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) + ->condition("{$embargo_alias}.expiration_date", $current_date, '<='); + + $existence_or->condition($embargo_and); + $existence_query->condition($existence_or); } } diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php new file mode 100644 index 0000000..8f1ce51 --- /dev/null +++ b/src/EmbargoExistenceQueryTrait.php @@ -0,0 +1,78 @@ +leftJoin('embargo', 'e', "%alias.embargoed_node = {$target_alias}.nid"); + $user_alias = $existence_query->leftJoin('embargo__exempt_users', 'u', "%alias.entity_id = {$embargo_alias}.id"); + $existence_or = $existence_query->orConditionGroup(); + + // No embargo. + // XXX: Might have to change to examine one of the fields outside the join + // condition? + $existence_or->isNull("{$embargo_alias}.embargoed_node"); + + // The user is exempt from the embargo. + $existence_or->condition("{$user_alias}.exempt_users_target_id", $this->user->id()); + + // ... 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)) { + $existence_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN'); + } + + // With embargo, without exemption. + $embargo_and = $existence_or->andConditionGroup(); + + // Has an embargo of a relevant type. + $embargo_and->condition("{$embargo_alias}.embargo_type", $embargo_types, 'IN'); + + $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') + ->fields('ue', ['embargoed_node']); + $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_and + ->condition( + "{$embargo_alias}.embargoed_node", + $unexpired_embargo_subquery, + 'NOT IN', + ) + ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) + ->condition("{$embargo_alias}.expiration_date", $current_date, '<='); + + $existence_or->condition($embargo_and); + $existence_query->condition($existence_or); + } + +} diff --git a/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php new file mode 100644 index 0000000..5371f70 --- /dev/null +++ b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php @@ -0,0 +1,149 @@ +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'), + ); + } + + /** + * {@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\SelectInterface $existence_query */ + $existence_query = $query->getMetaData('islandora_hierarchical_access_tagged_existence_query'); + $embargo_alias = $existence_query->leftJoin('embargo', 'e', '%alias.embargoed_node = lut.nid'); + $user_alias = $existence_query->leftJoin('embargo__exempt_users', 'u', "%alias.entity_id = {$embargo_alias}.id"); + $existence_or = $existence_query->orConditionGroup(); + + // No embargo. + // XXX: Might have to change to examine one of the fields outside the join + // condition? + $existence_or->isNull("{$embargo_alias}.embargoed_node"); + + // The user is exempt from the embargo. + $existence_or->condition("{$user_alias}.exempt_users_target_id", $this->user->id()); + + // ... 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)) { + $existence_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN'); + } + + // With embargo, without exemption. + $embargo_and = $existence_or->andConditionGroup(); + + // Has an embargo of a relevant type. + $embargo_and->condition( + "{$embargo_alias}.embargo_type", + match ($event->getType()) { + 'file', 'media' => [ + EmbargoInterface::EMBARGO_TYPE_FILE, + EmbargoInterface::EMBARGO_TYPE_NODE, + ], + 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE], + }, + 'IN', + ); + + $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') + ->fields('ue', ['embargoed_node']); + $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_and + ->condition( + "{$embargo_alias}.embargoed_node", + $unexpired_embargo_subquery, + 'NOT IN', + ) + ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) + ->condition("{$embargo_alias}.expiration_date", $current_date, '<='); + + $existence_or->condition($embargo_and); + $existence_query->condition($existence_or); + } + +} From 0e9937c7ea764eadb6980199eae2e37441114db3 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 6 Mar 2024 20:22:29 -0400 Subject: [PATCH 26/82] DRY'd out, still needs coding standards addressed. --- src/Access/QueryTagger.php | 160 +----------------- src/EmbargoExistenceQueryTrait.php | 54 +++++- ...ndoraHierarchicalAccessEventSubscriber.php | 66 +------- 3 files changed, 62 insertions(+), 218 deletions(-) diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index 6422f0d..bf713eb 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -8,10 +8,10 @@ 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; /** @@ -19,49 +19,9 @@ */ class QueryTagger { + use EmbargoExistenceQueryTrait; use QueryConjunctionTrait; - - /** - * The current user. - * - * @var \Drupal\Core\Session\AccountProxyInterface - */ - protected AccountProxyInterface $user; - - /** - * The IP of the request. - * - * @var string - */ - protected string $currentIp; - - /** - * Instance of a Drupal database connection. - * - * @var \Drupal\Core\Database\Connection - */ - protected Connection $database; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected EntityTypeManagerInterface $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. @@ -90,7 +50,8 @@ public function __construct( */ public function tagNode(SelectInterface $query) : void { if ($query->hasTag('islandora_hierarchical_access_subquery')) { - // Being run as a subquery, we do not want to add again. + // Being run as a subquery: We do not want to touch it as we expect our + // IslandoraHierarchicalAccessEventSubscriber class to deal with it. return; } if ($this->user->hasPermission('bypass embargo access')) { @@ -104,23 +65,9 @@ public function tagNode(SelectInterface $query) : void { $storage = $this->entityTypeManager->getStorage($type); $tables = $storage->getTableMapping()->getTableNames(); - $target_aliases = []; - $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 = (str_starts_with($info['table'], "{$type}__")) ? 'entity_id' : (substr($type, 0, 1) . "id"); - $alias = $info['alias']; - if (!in_array($alias, $tagged_table_aliases)) { - $tagged_table_aliases[] = $alias; - $target_aliases[] = "{$alias}.{$key}"; - } - } - } + $target_aliases = static::getTaggingTargets($query, $tagged_table_aliases, $tables, $type); if (empty($target_aliases)) { return; @@ -145,99 +92,8 @@ public function tagNode(SelectInterface $query) : void { if (!$query->hasTag('embargo_access')) { $query->addTag('embargo_access'); - $embargo_alias = $existence_query->leftJoin('embargo', 'e', '%alias.embargoed_node = existence_node.nid'); - $user_alias = $existence_query->leftJoin('embargo__exempt_users', 'u', "%alias.entity_id = {$embargo_alias}.id"); - $existence_or = $existence_query->orConditionGroup(); - - // No embargo. - // XXX: Might have to change to examine one of the fields outside the join - // condition? - $existence_or->isNull("{$embargo_alias}.embargoed_node"); - - // The user is exempt from the embargo. - $existence_or->condition("{$user_alias}.exempt_users_target_id", $this->user->id()); - - // ... 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)) { - $existence_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN'); - } - - // With embargo, without exemption. - $embargo_and = $existence_or->andConditionGroup(); - - // Has an embargo of a relevant type. - $embargo_and->condition("{$embargo_alias}.embargo_type", EmbargoInterface::EMBARGO_TYPE_NODE); - - $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') - ->fields('ue', ['embargoed_node']); - $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_and - ->condition( - "{$embargo_alias}.embargoed_node", - $unexpired_embargo_subquery, - 'NOT IN', - ) - ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) - ->condition("{$embargo_alias}.expiration_date", $current_date, '<='); - - $existence_or->condition($embargo_and); - $existence_query->condition($existence_or); - } - } - - /** - * 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(int $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'); + $this->applyExistenceQuery($existence_query, 'existence_node', [EmbargoInterface::EMBARGO_TYPE_NODE]); } - - // ... 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; } } diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php index 8f1ce51..7b6a9f8 100644 --- a/src/EmbargoExistenceQueryTrait.php +++ b/src/EmbargoExistenceQueryTrait.php @@ -1,6 +1,6 @@ leftJoin('embargo', 'e', "%alias.embargoed_node = {$target_alias}.nid"); $user_alias = $existence_query->leftJoin('embargo__exempt_users', 'u', "%alias.entity_id = {$embargo_alias}.id"); diff --git a/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php index 5371f70..7d46788 100644 --- a/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php +++ b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php @@ -8,7 +8,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; 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\Event\Event; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,14 +20,9 @@ */ class IslandoraHierarchicalAccessEventSubscriber implements EventSubscriberInterface, ContainerInjectionInterface { - const TAG = 'embargo_access'; + use EmbargoExistenceQueryTrait; - /** - * The IP of the current request. - * - * @var string|null - */ - protected ?string $currentIp; + const TAG = 'embargo_access'; /** * Constructor. @@ -86,64 +81,17 @@ public function processEvent(Event $event) : void { /** @var \Drupal\Core\Database\Query\SelectInterface $existence_query */ $existence_query = $query->getMetaData('islandora_hierarchical_access_tagged_existence_query'); - $embargo_alias = $existence_query->leftJoin('embargo', 'e', '%alias.embargoed_node = lut.nid'); - $user_alias = $existence_query->leftJoin('embargo__exempt_users', 'u', "%alias.entity_id = {$embargo_alias}.id"); - $existence_or = $existence_query->orConditionGroup(); - - // No embargo. - // XXX: Might have to change to examine one of the fields outside the join - // condition? - $existence_or->isNull("{$embargo_alias}.embargoed_node"); - - // The user is exempt from the embargo. - $existence_or->condition("{$user_alias}.exempt_users_target_id", $this->user->id()); - - // ... 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)) { - $existence_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN'); - } - - // With embargo, without exemption. - $embargo_and = $existence_or->andConditionGroup(); - - // Has an embargo of a relevant type. - $embargo_and->condition( - "{$embargo_alias}.embargo_type", + $this->applyExistenceQuery( + $existence_query, + 'lut', match ($event->getType()) { 'file', 'media' => [ EmbargoInterface::EMBARGO_TYPE_FILE, EmbargoInterface::EMBARGO_TYPE_NODE, ], 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE], - }, - 'IN', + } ); - - $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') - ->fields('ue', ['embargoed_node']); - $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_and - ->condition( - "{$embargo_alias}.embargoed_node", - $unexpired_embargo_subquery, - 'NOT IN', - ) - ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) - ->condition("{$embargo_alias}.expiration_date", $current_date, '<='); - - $existence_or->condition($embargo_and); - $existence_query->condition($existence_or); } } From 8bfa28cbb5a30198bdbe7764ff66d2c1319a9170 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 6 Mar 2024 20:32:35 -0400 Subject: [PATCH 27/82] Bit more DRYing. --- src/Access/QueryTagger.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index bf713eb..5b215db 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -61,13 +61,9 @@ public function tagNode(SelectInterface $query) : void { 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') ?? []; - $target_aliases = static::getTaggingTargets($query, $tagged_table_aliases, $tables, $type); + $target_aliases = $this->getTaggingTargets($query, $tagged_table_aliases, $type); if (empty($target_aliases)) { return; From 0231df313d0a6a3e5fb925bc610b3711710a0048 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 6 Mar 2024 20:36:05 -0400 Subject: [PATCH 28/82] Misc coding standards. --- .../src/Plugin/migrate/source/Entity.php | 8 +++----- src/EmbargoExistenceQueryTrait.php | 3 +++ tests/src/Kernel/EmbargoKernelTestBase.php | 2 +- tests/src/Kernel/IpRangeEmbargoTest.php | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) 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..864c45e 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; /** diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php index 7b6a9f8..db554cb 100644 --- a/src/EmbargoExistenceQueryTrait.php +++ b/src/EmbargoExistenceQueryTrait.php @@ -10,6 +10,9 @@ use Drupal\Core\Session\AccountProxyInterface; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; +/** + * Helper trait; facilitate filtering of embargoed entities. + */ trait EmbargoExistenceQueryTrait { /** 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/IpRangeEmbargoTest.php b/tests/src/Kernel/IpRangeEmbargoTest.php index 9b29a0d..60ef26a 100644 --- a/tests/src/Kernel/IpRangeEmbargoTest.php +++ b/tests/src/Kernel/IpRangeEmbargoTest.php @@ -2,9 +2,9 @@ namespace Drupal\Tests\embargo\Kernel; -use Drupal\node\NodeInterface; use Drupal\embargo\EmbargoInterface; use Drupal\embargo\IpRangeInterface; +use Drupal\node\NodeInterface; /** * Test IpRange embargo. From 5362f87ef17012f557fc5f101925bd3e93132090 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 6 Mar 2024 20:41:05 -0400 Subject: [PATCH 29/82] Attempt to pull grab the updated dependency. --- .github/workflows/phpunit.yml | 7 +++++++ 1 file changed, 7 insertions(+) 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" + } + } From 7f4e3827de41725cbba1b851373b030a34aaecef Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 7 Mar 2024 11:15:29 -0400 Subject: [PATCH 30/82] Explicitly acknowledge the media thumbnail file. Better than being left thinking there's a bug where we have undocumentedly expected there to be a file to exist that we didn't explicitly create in our `::setUp()`. --- .../EmbargoAccessQueryTaggingAlterTest.php | 85 ++++++++++++++++--- 1 file changed, 73 insertions(+), 12 deletions(-) diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php index 46eb041..a224e55 100644 --- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php +++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php @@ -4,6 +4,7 @@ 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; @@ -65,6 +66,35 @@ class EmbargoAccessQueryTaggingAlterTest extends EmbargoKernelTestBase { */ 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} */ @@ -78,6 +108,19 @@ public function setUp(): void { $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); } /** @@ -88,8 +131,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.'); - $this->assertEquals([$this->unembargoedNode->id()], array_column($result, 'nid')); + $this->assertCount(2, $result, 'User can only view non-embargoed nodes.'); + $this->assertEqualsCanonicalizing([ + $this->unembargoedNode->id(), + $this->unassociatedNode->id(), + ], array_column($result, 'nid')); } /** @@ -100,8 +146,11 @@ public function testEmbargoNodeQueryAlterAccess() { public function testNodeEmbargoReferencedMediaAccessQueryAlterAccessDenied() { $query = $this->generateMediaSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(1, $result, 'Media of embargoed nodes cannot be viewed'); - $this->assertEquals([$this->unembargoedMedia->id()], array_column($result, 'mid')); + $this->assertCount(2, $result, 'Media of embargoed nodes cannot be viewed'); + $this->assertEqualsCanonicalizing([ + $this->unembargoedMedia->id(), + $this->unassociatedMedia->id(), + ], array_column($result, 'mid')); } /** @@ -112,8 +161,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'); - $this->assertEquals([$this->unembargoedFile->id()], array_column($result, 'fid')); + $this->assertCount(3, $result, 'File of embargoed nodes cannot be viewed'); + $this->assertEqualsCanonicalizing([ + $this->unembargoedFile->id(), + $this->unassociatedFile->id(), + $this->mediaTypeDefaultFile->id(), + ], array_column($result, 'fid')); } /** @@ -128,10 +181,11 @@ public function testDeletedNodeEmbargoNodeAccessQueryAlterAccessAllowed() { $query = $this->generateNodeSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(2, $result, 'Non embargoed nodes can be viewed'); + $this->assertCount(3, $result, 'Non embargoed nodes can be viewed'); $this->assertEqualsCanonicalizing([ $this->embargoedNode->id(), $this->unembargoedNode->id(), + $this->unassociatedNode->id(), ], array_column($result, 'nid')); } @@ -146,11 +200,12 @@ public function testDeletedNodeEmbargoMediaAccessQueryAlterAccessAllowed() { $this->embargo->delete(); $query = $this->generateMediaSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(2, $result, + $this->assertCount(3, $result, 'Media of non embargoed nodes can be viewed'); $this->assertEqualsCanonicalizing([ $this->embargoedMedia->id(), $this->unembargoedMedia->id(), + $this->unassociatedMedia->id(), ], array_column($result, 'mid')); } @@ -166,11 +221,13 @@ public function testDeletedNodeEmbargoFileAccessQueryAlterAccessAllowed() { $query = $this->generateFileSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(2, $result, + $this->assertCount(4, $result, 'Files of non embargoed nodes can be viewed'); $this->assertEqualsCanonicalizing([ $this->embargoedFile->id(), $this->unembargoedFile->id(), + $this->unassociatedFile->id(), + $this->mediaTypeDefaultFile->id(), ], array_column($result, 'fid')); } @@ -184,9 +241,12 @@ public function testPublishScheduledEmbargoAccess() { $this->setEmbargoFutureUnpublishDate($this->embargo); $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); - $this->assertCount(1, $result, + $this->assertCount(2, $result, 'Node is still embargoed.'); - $this->assertEqualsCanonicalizing([$this->unembargoedNode->id()], array_column($result, 'nid')); + $this->assertEqualsCanonicalizing([ + $this->unembargoedNode->id(), + $this->unassociatedNode->id(), + ], array_column($result, 'nid')); } /** @@ -200,11 +260,12 @@ public function testUnpublishScheduledEmbargoAccess() { $this->setEmbargoPastUnpublishDate($this->embargo); $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); - $this->assertCount(2, $result, + $this->assertCount(3, $result, 'Embargo has been unpublished.'); $this->assertEqualsCanonicalizing([ $this->embargoedNode->id(), $this->unembargoedNode->id(), + $this->unassociatedNode->id(), ], array_column($result, 'nid')); } From 66490ad858707ad523c0fb10bebaf95dab9d6cfa Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 7 Mar 2024 12:25:25 -0400 Subject: [PATCH 31/82] Adjust to existence. --- src/EmbargoExistenceQueryTrait.php | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php index db554cb..61f26d6 100644 --- a/src/EmbargoExistenceQueryTrait.php +++ b/src/EmbargoExistenceQueryTrait.php @@ -97,7 +97,8 @@ protected function applyExistenceQuery(SelectInterface $existence_query, string $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') - ->fields('ue', ['embargoed_node']); + ->fields('ue', ['embargoed_node']) + ->where("ue.embargoed_node = {$embargo_alias}.embargoed_node"); $unexpired_embargo_subquery->condition($unexpired_embargo_subquery->orConditionGroup() ->condition('ue.expiration_type', EmbargoInterface::EXPIRATION_TYPE_INDEFINITE) ->condition($unexpired_embargo_subquery->andConditionGroup() @@ -106,11 +107,7 @@ protected function applyExistenceQuery(SelectInterface $existence_query, string ) ); $embargo_and - ->condition( - "{$embargo_alias}.embargoed_node", - $unexpired_embargo_subquery, - 'NOT IN', - ) + ->notExists($unexpired_embargo_subquery) ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) ->condition("{$embargo_alias}.expiration_date", $current_date, '<='); From e47a6a6e016c392a503f5557737b62988d01d1a4 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 7 Mar 2024 20:10:34 -0400 Subject: [PATCH 32/82] Update tests, make things more explicit. --- .../EmbargoAccessQueryTaggingAlterTest.php | 93 +++++++++---------- 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php index a224e55..2b568a3 100644 --- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php +++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php @@ -131,11 +131,11 @@ public function setUp(): void { public function testEmbargoNodeQueryAlterAccess() { $query = $this->generateNodeSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(2, $result, 'User can only view non-embargoed nodes.'); - $this->assertEqualsCanonicalizing([ - $this->unembargoedNode->id(), - $this->unassociatedNode->id(), - ], array_column($result, 'nid')); + + $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'); } /** @@ -146,11 +146,11 @@ public function testEmbargoNodeQueryAlterAccess() { public function testNodeEmbargoReferencedMediaAccessQueryAlterAccessDenied() { $query = $this->generateMediaSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(2, $result, 'Media of embargoed nodes cannot be viewed'); - $this->assertEqualsCanonicalizing([ - $this->unembargoedMedia->id(), - $this->unassociatedMedia->id(), - ], array_column($result, 'mid')); + + $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'); } /** @@ -161,12 +161,12 @@ public function testNodeEmbargoReferencedMediaAccessQueryAlterAccessDenied() { public function testNodeEmbargoReferencedFileAccessQueryAlterAccessDenied() { $query = $this->generateFileSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(3, $result, 'File of embargoed nodes cannot be viewed'); - $this->assertEqualsCanonicalizing([ - $this->unembargoedFile->id(), - $this->unassociatedFile->id(), - $this->mediaTypeDefaultFile->id(), - ], array_column($result, 'fid')); + + $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'); } /** @@ -181,12 +181,10 @@ public function testDeletedNodeEmbargoNodeAccessQueryAlterAccessAllowed() { $query = $this->generateNodeSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(3, $result, 'Non embargoed nodes can be viewed'); - $this->assertEqualsCanonicalizing([ - $this->embargoedNode->id(), - $this->unembargoedNode->id(), - $this->unassociatedNode->id(), - ], array_column($result, 'nid')); + $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'); } /** @@ -200,13 +198,11 @@ public function testDeletedNodeEmbargoMediaAccessQueryAlterAccessAllowed() { $this->embargo->delete(); $query = $this->generateMediaSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(3, $result, - 'Media of non embargoed nodes can be viewed'); - $this->assertEqualsCanonicalizing([ - $this->embargoedMedia->id(), - $this->unembargoedMedia->id(), - $this->unassociatedMedia->id(), - ], array_column($result, 'mid')); + + $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'); } /** @@ -218,17 +214,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(4, $result, - 'Files of non embargoed nodes can be viewed'); - $this->assertEqualsCanonicalizing([ - $this->embargoedFile->id(), - $this->unembargoedFile->id(), - $this->unassociatedFile->id(), - $this->mediaTypeDefaultFile->id(), - ], array_column($result, 'fid')); + + $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'); } /** @@ -241,12 +235,11 @@ public function testPublishScheduledEmbargoAccess() { $this->setEmbargoFutureUnpublishDate($this->embargo); $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); - $this->assertCount(2, $result, - 'Node is still embargoed.'); - $this->assertEqualsCanonicalizing([ - $this->unembargoedNode->id(), - $this->unassociatedNode->id(), - ], array_column($result, 'nid')); + + $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'); } /** @@ -260,13 +253,11 @@ public function testUnpublishScheduledEmbargoAccess() { $this->setEmbargoPastUnpublishDate($this->embargo); $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); - $this->assertCount(3, $result, - 'Embargo has been unpublished.'); - $this->assertEqualsCanonicalizing([ - $this->embargoedNode->id(), - $this->unembargoedNode->id(), - $this->unassociatedNode->id(), - ], array_column($result, 'nid')); + + $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'); } } From 3bac38fea9bf54533ee2415a4a1499c9599f2237 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 7 Mar 2024 20:11:29 -0400 Subject: [PATCH 33/82] Adjust for altered condition structures. --- src/Access/QueryTagger.php | 10 +++++++++- src/EmbargoExistenceQueryTrait.php | 15 ++++++++++++--- ...IslandoraHierarchicalAccessEventSubscriber.php | 5 ++++- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index 5b215db..907153a 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -88,7 +88,15 @@ public function tagNode(SelectInterface $query) : void { if (!$query->hasTag('embargo_access')) { $query->addTag('embargo_access'); - $this->applyExistenceQuery($existence_query, 'existence_node', [EmbargoInterface::EMBARGO_TYPE_NODE]); + $existence_query->condition($existence_condition = $existence_query->andConditionGroup()) + ->addMetaData('embargo_existence_condition', $existence_condition); + + $this->applyExistenceQuery( + $existence_query, + $existence_condition, + 'existence_node', + [EmbargoInterface::EMBARGO_TYPE_NODE], + ); } } diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php index 61f26d6..92cdaa8 100644 --- a/src/EmbargoExistenceQueryTrait.php +++ b/src/EmbargoExistenceQueryTrait.php @@ -4,6 +4,7 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Query\ConditionInterface; use Drupal\Core\Database\Query\SelectInterface; use Drupal\Core\Datetime\DateFormatterInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -67,10 +68,19 @@ trait EmbargoExistenceQueryTrait { * @param array $embargo_types * The types of embargo to deal with. */ - protected function applyExistenceQuery(SelectInterface $existence_query, string $target_alias, array $embargo_types) { + protected function applyExistenceQuery( + SelectInterface $existence_query, + ConditionInterface $existence_condition, + string $target_alias, + array $embargo_types, + ) { $embargo_alias = $existence_query->leftJoin('embargo', 'e', "%alias.embargoed_node = {$target_alias}.nid"); $user_alias = $existence_query->leftJoin('embargo__exempt_users', 'u', "%alias.entity_id = {$embargo_alias}.id"); - $existence_or = $existence_query->orConditionGroup(); + + + $existence_condition->condition( + $existence_or = $existence_condition->orConditionGroup() + ); // No embargo. // XXX: Might have to change to examine one of the fields outside the join @@ -112,7 +122,6 @@ protected function applyExistenceQuery(SelectInterface $existence_query, string ->condition("{$embargo_alias}.expiration_date", $current_date, '<='); $existence_or->condition($embargo_and); - $existence_query->condition($existence_or); } } diff --git a/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php index 7d46788..532836d 100644 --- a/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php +++ b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php @@ -81,8 +81,11 @@ public function processEvent(Event $event) : void { /** @var \Drupal\Core\Database\Query\SelectInterface $existence_query */ $existence_query = $query->getMetaData('islandora_hierarchical_access_tagged_existence_query'); + /** @var \Drupal\Core\Database\Query\ConditionInterface $existence_condition */ + $existence_condition = $existence_query->getMetaData('islandora_hierarchical_access_tagged_existence_condition'); $this->applyExistenceQuery( $existence_query, + $existence_condition, 'lut', match ($event->getType()) { 'file', 'media' => [ @@ -90,7 +93,7 @@ public function processEvent(Event $event) : void { EmbargoInterface::EMBARGO_TYPE_NODE, ], 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE], - } + }, ); } From 7507e6ba37144c1328014e7fb78c58c64c87ac65 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Sat, 9 Mar 2024 13:33:36 -0400 Subject: [PATCH 34/82] Functional. --- src/Access/QueryTagger.php | 29 +----- src/EmbargoExistenceQueryTrait.php | 97 +++++++++++++------ ...ndoraHierarchicalAccessEventSubscriber.php | 7 +- 3 files changed, 68 insertions(+), 65 deletions(-) diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index 907153a..202c158 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -70,34 +70,7 @@ public function tagNode(SelectInterface $query) : void { } $query->addMetaData('embargo_tagged_table_aliases', $tagged_table_aliases); - $existence_query = $query->getMetaData('embargo_tagged_existence_query'); - - if (!$existence_query) { - $existence_query = $this->database->select('node', 'existence_node'); - $existence_query->fields('existence_node', ['nid']); - $query->addMetaData('embargo_tagged_existence_query', $existence_query); - - $query->exists($existence_query); - } - - $existence_query->where(strtr('!field IN (!targets)', [ - '!field' => 'existence_node.nid', - '!targets' => implode(', ', $target_aliases), - ])); - - if (!$query->hasTag('embargo_access')) { - $query->addTag('embargo_access'); - - $existence_query->condition($existence_condition = $existence_query->andConditionGroup()) - ->addMetaData('embargo_existence_condition', $existence_condition); - - $this->applyExistenceQuery( - $existence_query, - $existence_condition, - 'existence_node', - [EmbargoInterface::EMBARGO_TYPE_NODE], - ); - } + $this->applyExistenceQuery($query, $target_aliases, [EmbargoInterface::EMBARGO_TYPE_NODE]); } } diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php index 92cdaa8..2ec2b8b 100644 --- a/src/EmbargoExistenceQueryTrait.php +++ b/src/EmbargoExistenceQueryTrait.php @@ -61,54 +61,83 @@ trait EmbargoExistenceQueryTrait { /** * Helper; apply existence checks to a node(-like) table. * - * @param \Drupal\Core\Database\Query\SelectInterface $existence_query - * The query to which to add. - * @param string $target_alias + * @param string[] $target_aliases * The alias of the node-like table in the query to which to attach things. * @param array $embargo_types * The types of embargo to deal with. */ protected function applyExistenceQuery( - SelectInterface $existence_query, ConditionInterface $existence_condition, - string $target_alias, + array $target_aliases, array $embargo_types, - ) { - $embargo_alias = $existence_query->leftJoin('embargo', 'e', "%alias.embargoed_node = {$target_alias}.nid"); - $user_alias = $existence_query->leftJoin('embargo__exempt_users', 'u', "%alias.entity_id = {$embargo_alias}.id"); - - + ) : void { $existence_condition->condition( - $existence_or = $existence_condition->orConditionGroup() + $existence_condition->orConditionGroup() + ->notExists($this->getNullQuery($target_aliases, $embargo_types)) + ->exists($this->getAccessibleEmbargoesQuery($target_aliases, $embargo_types)) ); + } + + 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->where(strtr('!field IN (!targets)', [ + '!field' => "{$embargo_alias}.embargoed_node", + '!targets' => implode(', ', $target_aliases), + ])); + $query->condition("{$embargo_alias}.embargo_type", $embargo_types, 'IN'); + + return $query; + } - // No embargo. - // XXX: Might have to change to examine one of the fields outside the join - // condition? - $existence_or->isNull("{$embargo_alias}.embargoed_node"); + 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); + + $replacements = [ + '!field' => "{$embargo_alias}.embargoed_node", + '!targets' => implode(', ', $target_aliases), + ]; + $embargo_existence->condition( + $embargo_existence->orConditionGroup() + ->condition($existence_condition = $embargo_existence->andConditionGroup() + ->where(strtr('!field IN (!targets)', $replacements)) + ->condition($embargo_or = $embargo_existence->orConditionGroup()) + ) + ); - // The user is exempt from the embargo. - $existence_or->condition("{$user_alias}.exempt_users_target_id", $this->user->id()); + $embargo_existence->addMetaData('embargo_existence_condition', $existence_condition); - // ... the incoming IP is in an exempt range; or... + // - 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 (!empty($applicable_ip_ranges)) { - $existence_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN'); + if ($applicable_ip_ranges) { + $embargo_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN'); } - // With embargo, without exemption. - $embargo_and = $existence_or->andConditionGroup(); - - // Has an embargo of a relevant type. - $embargo_and->condition("{$embargo_alias}.embargo_type", $embargo_types, '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') - ->fields('ue', ['embargoed_node']) - ->where("ue.embargoed_node = {$embargo_alias}.embargoed_node"); + ->where("ue.embargoed_node = {$embargo_alias}.embargoed_node") + ->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() @@ -116,12 +145,16 @@ protected function applyExistenceQuery( ->condition('ue.expiration_date', $current_date, '>') ) ); - $embargo_and - ->notExists($unexpired_embargo_subquery) - ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) - ->condition("{$embargo_alias}.expiration_date", $current_date, '<='); - $existence_or->condition($embargo_and); + $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/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php index 532836d..bad9e35 100644 --- a/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php +++ b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php @@ -79,14 +79,11 @@ public function processEvent(Event $event) : void { return; } - /** @var \Drupal\Core\Database\Query\SelectInterface $existence_query */ - $existence_query = $query->getMetaData('islandora_hierarchical_access_tagged_existence_query'); /** @var \Drupal\Core\Database\Query\ConditionInterface $existence_condition */ - $existence_condition = $existence_query->getMetaData('islandora_hierarchical_access_tagged_existence_condition'); + $existence_condition = $query->getMetaData('islandora_hierarchical_access_tagged_existence_condition'); $this->applyExistenceQuery( - $existence_query, $existence_condition, - 'lut', + ['lut_exist.nid'], match ($event->getType()) { 'file', 'media' => [ EmbargoInterface::EMBARGO_TYPE_FILE, From 19bb47c2d7c389a2b19169bbd78ef354e51b3464 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Sat, 9 Mar 2024 13:36:47 -0400 Subject: [PATCH 35/82] Coding standards. --- src/EmbargoExistenceQueryTrait.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php index 2ec2b8b..de6b77c 100644 --- a/src/EmbargoExistenceQueryTrait.php +++ b/src/EmbargoExistenceQueryTrait.php @@ -61,6 +61,8 @@ trait EmbargoExistenceQueryTrait { /** * Helper; apply existence checks to a node(-like) table. * + * @param \Drupal\Core\Database\Query\ConditionInterface $existence_condition + * The condition object to which to add the existence check. * @param string[] $target_aliases * The alias of the node-like table in the query to which to attach things. * @param array $embargo_types @@ -78,6 +80,17 @@ protected function applyExistenceQuery( ); } + /** + * 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); @@ -92,6 +105,17 @@ protected function getNullQuery(array $target_aliases, array $embargo_types) : S 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'; From 19abd0367826497b48df6274bda638527ce0c5dc Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Sat, 9 Mar 2024 16:14:42 -0400 Subject: [PATCH 36/82] Test out the query tagging for IP range statements. Bit of formatting, and using constants. --- tests/src/Kernel/IpRangeEmbargoTest.php | 29 +++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/src/Kernel/IpRangeEmbargoTest.php b/tests/src/Kernel/IpRangeEmbargoTest.php index 60ef26a..a858416 100644 --- a/tests/src/Kernel/IpRangeEmbargoTest.php +++ b/tests/src/Kernel/IpRangeEmbargoTest.php @@ -5,6 +5,7 @@ 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'); + } + } From be8a746805a79ad11ecb0d41f32110025fcecc72 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Sat, 9 Mar 2024 16:34:28 -0400 Subject: [PATCH 37/82] Roll some tests where multiple embargoes are involved. Other embargoes block access (but exemptions from _any_ are let through). --- .../EmbargoAccessQueryTaggingAlterTest.php | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php index 2b568a3..82c912d 100644 --- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php +++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php @@ -231,7 +231,7 @@ 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); $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); @@ -243,7 +243,7 @@ public function testPublishScheduledEmbargoAccess() { } /** - * Tests embargo scheduled to be unpublished in the past. + * Test embargo scheduled in the past, without any other embargo. * * @throws \Drupal\Core\Entity\EntityStorageException */ @@ -260,4 +260,46 @@ public function testUnpublishScheduledEmbargoAccess() { $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'); + } + } From 3477650c121129cdebe824be902d142c564b8fd1 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 11 Mar 2024 11:43:15 -0300 Subject: [PATCH 38/82] Back over to property, instead of a calculated field. Calculated field was getting hit by context, but also leading to some other oddities. Easier just to roll back into the processor, for now. --- embargo.module | 36 -------- .../search_api/processor/EmbargoProcessor.php | 89 ++++++++++++++++--- .../ListableEntityProcessorProperty.php | 34 +++++++ 3 files changed, 111 insertions(+), 48 deletions(-) create mode 100644 src/Plugin/search_api/processor/Property/ListableEntityProcessorProperty.php diff --git a/embargo.module b/embargo.module index 0874bf1..5026ee0 100644 --- a/embargo.module +++ b/embargo.module @@ -8,12 +8,8 @@ use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; -use Drupal\Core\Entity\EntityTypeInterface; -use Drupal\Core\Field\BaseFieldDefinition; -use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Session\AccountInterface; use Drupal\embargo\EmbargoInterface; -use Drupal\embargo\EmbargoItemList; use Drupal\embargo\EmbargoStorage; use Drupal\islandora_hierarchical_access\LUTGeneratorInterface; @@ -105,38 +101,6 @@ function embargo_theme($existing, $type, $theme, $path) { ]; } -/** - * Implements hook_entity_base_field_info(). - */ -function embargo_entity_base_field_info(EntityTypeInterface $entity_type) { - if (!in_array($entity_type->id(), ['file', 'media', 'node'])) { - return []; - } - - $fields = []; - - $fields['embargo'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Embargoes')) - ->setDescription(t('Embargoes affecting this item.')) - ->setTranslatable(FALSE) - ->setRevisionable(FALSE) - ->setRequired(FALSE) - ->setDisplayConfigurable('view', FALSE) - ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) - ->setComputed(TRUE) - ->setClass(EmbargoItemList::class) - ->setSetting('target_type', 'embargo') - ->setSetting('embargo_types', match($entity_type->id()) { - 'file', 'media' => [ - EmbargoInterface::EMBARGO_TYPE_NODE, - EmbargoInterface::EMBARGO_TYPE_FILE, - ], - 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE], - }); - - return $fields; -} - /** * Implements hook_ENTITY_TYPE_insert() for embargo entities. */ diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 6e3852f..82f9613 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -4,14 +4,19 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Cache\RefinableCacheableDependencyInterface; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\embargo\EmbargoInterface; +use Drupal\embargo\Plugin\search_api\processor\Property\ListableEntityProcessorProperty; use Drupal\search_api\Datasource\DatasourceInterface; +use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; use Drupal\search_api\Query\ConditionGroupInterface; use Drupal\search_api\Query\QueryInterface; +use Drupal\search_api\SearchApiException; +use Drupal\search_api\Utility\Utility; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RequestStack; @@ -24,6 +29,7 @@ * description = @Translation("Add information regarding embargo access * constraints."), * stages = { + * "add_properties" = 20, * "pre_index_save" = 20, * "preprocess_query" = 20, * }, @@ -77,10 +83,69 @@ public static function create(ContainerInterface $container, array $configuratio return $instance; } + /** + * {@inheritdoc} + */ + public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array { + if ($datasource === NULL) { + return []; + } + + return [ + 'embargo' => ListableEntityProcessorProperty::create('embargo') + ->setList() + ->setProcessorId($this->getPluginId()), + ]; + } + + /** + * {@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); + + foreach ($embargoes as $embargo) { + $this->getFieldsHelper()->extractFields($embargo->getTypedData(), $to_extract); + } + + } + /** * {@inheritDoc} */ - public function preIndexSave() { + public function preIndexSave() : void { parent::preIndexSave(); foreach ($this->index->getDatasources() as $datasource_id => $datasource) { @@ -88,12 +153,12 @@ public function preIndexSave() { continue; } - $this->ensureField($datasource_id, 'embargo:entity:id', 'integer'); - $this->ensureField($datasource_id, 'embargo:entity:embargo_type', 'integer'); - $this->ensureField($datasource_id, 'embargo:entity:expiration_date', 'date'); - $this->ensureField($datasource_id, 'embargo:entity:expiration_type', 'integer'); - $this->ensureField($datasource_id, 'embargo:entity:exempt_ips:entity:id', 'integer'); - $this->ensureField($datasource_id, 'embargo:entity:exempt_users:entity:uid', 'integer'); + $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'); } } @@ -142,19 +207,19 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer ]); // No embargo. - if ($field = $this->findField($datasource_id, 'embargo:entity:id')) { + 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:entity:expiration_type')) { + 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:entity:expiration_date')) { + 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()), '<='); @@ -173,12 +238,12 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer if ($this->currentUser->isAnonymous()) { $query->addCacheContexts(['user.roles:anonymous']); } - elseif ($field = $this->findField($datasource_id, 'embargo:entity:exempt_users:entity:uid')) { + 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:entity:exempt_ips:entity:id')) { + 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() 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; + } + +} From b145a03f7b993c769afbbb7a791c5903bd17be35 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Fri, 15 Mar 2024 18:21:58 -0300 Subject: [PATCH 39/82] Ensure labels exist for the auto-generated fields. These fields get listed in the views config interface, even if we set them to be hidden, so let's ensure that they at least have a label. --- .../search_api/processor/EmbargoProcessor.php | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 82f9613..626f907 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -6,11 +6,14 @@ use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Field\EntityReferenceFieldItemListInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\embargo\EmbargoInterface; use Drupal\embargo\Plugin\search_api\processor\Property\ListableEntityProcessorProperty; use Drupal\search_api\Datasource\DatasourceInterface; +use Drupal\search_api\IndexInterface; +use Drupal\search_api\Item\FieldInterface; use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; use Drupal\search_api\Query\ConditionGroupInterface; @@ -148,17 +151,41 @@ public function addFieldValues(ItemInterface $item) : void { 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; } - $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'); + $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); } } From b1e8b3308511857dad488af6d169523c0de90c87 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 25 Mar 2024 13:32:12 -0300 Subject: [PATCH 40/82] Theorhetical, better caching. --- embargo.services.yml | 7 ++ src/Cache/Context/IpRangeCacheContext.php | 81 +++++++++++++++++++ src/Entity/Embargo.php | 34 +++++--- src/Entity/IpRange.php | 10 +++ .../search_api/processor/EmbargoProcessor.php | 2 +- 5 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 src/Cache/Context/IpRangeCacheContext.php diff --git a/embargo.services.yml b/embargo.services.yml index b316143..a7470c3 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -38,3 +38,10 @@ services: - '@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 } 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/Entity/Embargo.php b/src/Entity/Embargo.php index 03f8b57..1201540 100644 --- a/src/Entity/Embargo.php +++ b/src/Entity/Embargo.php @@ -378,33 +378,49 @@ public function setEmbargoedNode(NodeInterface $node): EmbargoInterface { } /** - * The maximum age for which this object may be cached. - * - * @return int - * The maximum time in seconds that this object may be cached. + * {@inheritDoc} */ public function getCacheMaxAge() { + $max_age = parent::getCacheMaxAge(); + $now = time(); // Invalidate cache after a scheduled embargo expires. if ($this->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(), + [$this->getExemptUsers() ? 'user' : 'user.permissions'], + ); + + if ($this->getExemptIps()) { + $contexts = Cache::mergeContexts($contexts, $this->getExemptIps()->getCacheContexts()); + } + + return $contexts; + } + /** * {@inheritdoc} */ diff --git a/src/Entity/IpRange.php b/src/Entity/IpRange.php index 60bea11..1590688 100644 --- a/src/Entity/IpRange.php +++ b/src/Entity/IpRange.php @@ -3,6 +3,7 @@ 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; @@ -239,4 +240,13 @@ public static function isValidCidr(string $cidr): bool { return FALSE; } + /** + * {@inheritDoc} + */ + public function getCacheContexts() { + return Cache::mergeContexts(parent::getCacheContexts(), [ + 'ip.embargo_range', + ]); + } + } diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 82f9613..d1b6a64 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -251,7 +251,7 @@ protected function addEmbargoFilters(string $datasource_id, QueryInterface $quer $or_group->addCondition($field->getFieldIdentifier(), $ipRange->id()); $query->addCacheableDependency($ipRange); } - $query->addCacheContexts(['ip']); + $query->addCacheContexts(['ip.embargo_range']); } return (count($or_group->getConditions()) > 0) ? $or_group : NULL; From 27a93f8e64a6956179cb512b741f1a6c3c7397e9 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 3 Apr 2024 11:18:02 -0300 Subject: [PATCH 41/82] Move to trait. --- src/EmbargoStorage.php | 100 ++--------------------------- src/EmbargoStorageInterface.php | 8 +++ src/EmbargoStorageTrait.php | 109 ++++++++++++++++++++++++++++++++ 3 files changed, 121 insertions(+), 96 deletions(-) create mode 100644 src/EmbargoStorageTrait.php 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..f8292ce --- /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 $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; + } + +} From 41f1f7d51ae56e9ad39c8de6d6b65e5ab0d59177 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 3 Apr 2024 13:17:31 -0300 Subject: [PATCH 42/82] Move over to tracker class. --- embargo.module | 71 ++++++------------------- embargo.services.yml | 6 +++ src/SearchApiTracker.php | 111 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+), 55 deletions(-) create mode 100644 src/SearchApiTracker.php diff --git a/embargo.module b/embargo.module index ff0a01e..7d020c4 100644 --- a/embargo.module +++ b/embargo.module @@ -6,12 +6,10 @@ */ use Drupal\Core\Database\Query\AlterableInterface; -use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\embargo\EmbargoInterface; use Drupal\embargo\EmbargoStorage; -use Drupal\islandora_hierarchical_access\LUTGeneratorInterface; +use Drupal\node\NodeInterface; /** * Implements hook_entity_type_alter(). @@ -87,72 +85,35 @@ function embargo_theme($existing, $type, $theme, $path) { * Implements hook_ENTITY_TYPE_insert() for embargo entities. */ function embargo_embargo_insert(EntityInterface $entity) : void { - _embargo_search_api_track($entity); + /** @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 { - _embargo_search_api_track($entity); + /** @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 { - _embargo_search_api_track($entity); + /** @var \Drupal\embargo\SearchApiTracker $tracker */ + $tracker = \Drupal::service('embargo.search_api_tracker_helper'); + $tracker->track($entity); } /** - * Helper; deal with updating indexes of related items. - * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The embargo instance. + * Implements hook_ENTITY_TYPE_delete() for node entities. */ -function _embargo_search_api_track(EntityInterface $entity) : void { - assert($entity instanceof EmbargoInterface); - if (!\Drupal::moduleHandler()->moduleExists('search_api')) { - return; - } - - // On updates, deal with the original value, in addition to the new. - if (isset($entity->original)) { - _embargo_search_api_track($entity->original); - } - - if (!($node = $entity->getEmbargoedNode())) { - // No embargoed node? - return; - } - - /** @var \Drupal\search_api\Plugin\search_api\datasource\ContentEntityTrackingManager $tracking_manager */ - $tracking_manager = \Drupal::getContainer()->get('search_api.entity_datasource.tracking_manager'); - /** @var \Drupal\search_api\Utility\TrackingHelperInterface $tracking_helper */ - $tracking_helper = \Drupal::getContainer()->get('search_api.tracking_helper'); - - $track = function (ContentEntityInterface $entity) use ($tracking_manager, $tracking_helper) { - $tracking_manager->trackEntityChange($entity); - $tracking_helper->trackReferencedEntityUpdate($entity); - }; - - $track($node); - - $results = \Drupal::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)); - - $entity_type_manager = \Drupal::entityTypeManager(); - /** @var \Drupal\media\MediaInterface $media */ - foreach ($entity_type_manager->getStorage('media')->loadMultiple($media_ids) as $media) { - $track($media); - } - /** @var \Drupal\file\FileInterface $file */ - foreach ($entity_type_manager->getStorage('file')->loadMultiple($file_ids) as $file) { - $track($file); - } - +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); } diff --git a/embargo.services.yml b/embargo.services.yml index b316143..c6e08a3 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -38,3 +38,9 @@ services: - '@service_container' tags: - { name: 'event_subscriber' } + embargo.search_api_tracker_helper: + class: Drupal\embargo\SearchApiTracker + factory: [null, 'create'] + arguments: + - '@service_container' + diff --git a/src/SearchApiTracker.php b/src/SearchApiTracker.php new file mode 100644 index 0000000..8937472 --- /dev/null +++ b/src/SearchApiTracker.php @@ -0,0 +1,111 @@ +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'), + ); + } + + /** + * 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->moduleHandler->moduleExists('search_api')) { + 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. + */ + protected function doTrack(ContentEntityInterface $entity) : void { + $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); + } + } + +} From 2f2d43d0b8b1ef8385ee80a6f2efa0feb14a2264 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 3 Apr 2024 13:20:39 -0300 Subject: [PATCH 43/82] Drop extra newline. --- embargo.services.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/embargo.services.yml b/embargo.services.yml index c6e08a3..e5acd21 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -43,4 +43,3 @@ services: factory: [null, 'create'] arguments: - '@service_container' - From 3ffce3df3d604a584bf1bf0d4fb2a047b6979be2 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 3 Apr 2024 14:43:25 -0300 Subject: [PATCH 44/82] Add return typehints. --- embargo.module | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/embargo.module b/embargo.module index 7d020c4..0fd208e 100644 --- a/embargo.module +++ b/embargo.module @@ -5,6 +5,7 @@ * Hook implementations. */ +use Drupal\Core\Access\AccessResultInterface; use Drupal\Core\Database\Query\AlterableInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Session\AccountInterface; @@ -14,7 +15,7 @@ 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]; @@ -25,7 +26,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); @@ -34,7 +35,7 @@ function embargo_entity_access(EntityInterface $entity, $operation, AccountInter /** * Implements hook_file_download(). */ -function embargo_file_download($uri) { +function embargo_file_download($uri) : array|int { $files = \Drupal::entityTypeManager() ->getStorage('file') ->loadByProperties(['uri' => $uri]); @@ -50,7 +51,7 @@ function embargo_file_download($uri) { /** * Implements hook_query_TAG_alter() for `node_access` tagged queries. */ -function embargo_query_node_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->tagNode($query); @@ -59,7 +60,7 @@ function embargo_query_node_access_alter(AlterableInterface $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', From 8bcda9518f4456352156aae4f255f8ebaebac007 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 3 Apr 2024 14:46:10 -0300 Subject: [PATCH 45/82] Can also be null. --- embargo.module | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embargo.module b/embargo.module index 0fd208e..31f4365 100644 --- a/embargo.module +++ b/embargo.module @@ -35,7 +35,7 @@ function embargo_entity_access(EntityInterface $entity, $operation, AccountInter /** * Implements hook_file_download(). */ -function embargo_file_download($uri) : array|int { +function embargo_file_download($uri) : null|array|int { $files = \Drupal::entityTypeManager() ->getStorage('file') ->loadByProperties(['uri' => $uri]); From b7449259294cb080ff91d7e373401fd227c9c28b Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 3 Apr 2024 16:40:18 -0300 Subject: [PATCH 46/82] Relay index tracking down to files, as necessary. --- embargo.module | 34 +++++++++++ src/SearchApiTracker.php | 128 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 1 deletion(-) diff --git a/embargo.module b/embargo.module index 31f4365..b9dec98 100644 --- a/embargo.module +++ b/embargo.module @@ -10,6 +10,7 @@ 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; /** @@ -118,3 +119,36 @@ function embargo_node_delete(EntityInterface $entity) : void { $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/src/SearchApiTracker.php b/src/SearchApiTracker.php index 8937472..93434ee 100644 --- a/src/SearchApiTracker.php +++ b/src/SearchApiTracker.php @@ -8,7 +8,11 @@ use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\file\FileInterface; +use Drupal\islandora\IslandoraUtils; use Drupal\islandora_hierarchical_access\LUTGeneratorInterface; +use Drupal\media\MediaInterface; +use Drupal\media\MediaTypeInterface; use Drupal\node\NodeInterface; use Drupal\search_api\Plugin\search_api\datasource\ContentEntityTrackingManager; use Drupal\search_api\Utility\TrackingHelperInterface; @@ -79,7 +83,7 @@ public function track(EntityInterface $entity) : void { * @param \Drupal\Core\Entity\ContentEntityInterface $entity * The entity to track. */ - protected function doTrack(ContentEntityInterface $entity) : void { + public function doTrack(ContentEntityInterface $entity) : void { $this->trackingManager->trackEntityChange($entity); $this->trackingHelper->trackReferencedEntityUpdate($entity); } @@ -108,4 +112,126 @@ public function propagateChildren(NodeInterface $node) : void { } } + /** + * 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 = $media->getEntityType(); + 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 { + // 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_type->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 $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( + $this->getMediaType($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); + return $containers?->referencedEntities() ?? []; + } + + /** + * 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 = array_values($this->getMediaContainers($media->original ?? NULL)); + $current_containers = array_values($this->getMediaContainers($media)); + + $same_containers = $current_containers == array_intersect($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); + } + } + } From 787ae75ea173f32b193b831c499843a3b3f9107a Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 3 Apr 2024 17:11:34 -0300 Subject: [PATCH 47/82] Adjust media_type loading. --- src/SearchApiTracker.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/SearchApiTracker.php b/src/SearchApiTracker.php index 93434ee..b70e337 100644 --- a/src/SearchApiTracker.php +++ b/src/SearchApiTracker.php @@ -84,6 +84,9 @@ public function track(EntityInterface $entity) : void { * The entity to track. */ public function doTrack(ContentEntityInterface $entity) : void { + if (!$this->moduleHandler->moduleExists('search_api')) { + return; + } $this->trackingManager->trackEntityChange($entity); $this->trackingHelper->trackReferencedEntityUpdate($entity); } @@ -122,7 +125,7 @@ public function propagateChildren(NodeInterface $node) : void { * The media type of the given media. */ protected function getMediaType(MediaInterface $media) : MediaTypeInterface { - $type = $media->getEntityType(); + $type = $this->entityTypeManager->getStorage('media_type')->load($media->bundle()); assert($type instanceof MediaTypeInterface); return $type; } @@ -147,7 +150,7 @@ public function isMediaRelevant(MediaInterface $media) : bool { } $media_type = $this->getMediaType($media); - $media_source = $media_type->getSource(); + $media_source = $media->getSource(); if ($media_source->getSourceFieldDefinition($media_type)->getSetting('target_type') !== 'file') { return FALSE; } @@ -158,16 +161,16 @@ public function isMediaRelevant(MediaInterface $media) : bool { /** * Get the file for the media. * - * @param \Drupal\media\MediaInterface $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 { + public function mediaGetFile(?MediaInterface $media) : ?FileInterface { return $media ? $this->entityTypeManager->getStorage('file')->load( - $this->getMediaType($media)->getSource()->getSourceFieldValue($media) + $media->getSource()->getSourceFieldValue($media) ) : NULL; } From de3eb891099ccffb4d721eb5076bcf4a4f6ae9e2 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 4 Apr 2024 16:48:18 -0300 Subject: [PATCH 48/82] Fix up intersection. --- src/SearchApiTracker.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/SearchApiTracker.php b/src/SearchApiTracker.php index b70e337..cbd7298 100644 --- a/src/SearchApiTracker.php +++ b/src/SearchApiTracker.php @@ -187,7 +187,12 @@ public function mediaGetFile(?MediaInterface $media) : ?FileInterface { protected function getMediaContainers(?MediaInterface $media) : array { /** @var \Drupal\Core\Field\EntityReferenceFieldItemList|null $containers */ $containers = $media?->get(IslandoraUtils::MEDIA_OF_FIELD); - return $containers?->referencedEntities() ?? []; + $entities = $containers?->referencedEntities() ?? []; + $to_return = []; + foreach ($entities as $entity) { + $to_return[$entity->id()] = $entity; + } + return $to_return; } /** @@ -206,10 +211,10 @@ public function mediaWriteReaction(MediaInterface $media) : void { $same_file = $original_file === $current_file; - $original_containers = array_values($this->getMediaContainers($media->original ?? NULL)); - $current_containers = array_values($this->getMediaContainers($media)); + $original_containers = $this->getMediaContainers($media->original ?? NULL); + $current_containers = $this->getMediaContainers($media); - $same_containers = $current_containers == array_intersect($current_containers, $original_containers); + $same_containers = $current_containers == array_intersect_key($current_containers, $original_containers); if (!($same_file && $same_containers)) { if ($original_file) { From 209c7cece8bcc9c7ffd2329c4a4345bd82dd4c4e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 8 Apr 2024 11:39:17 -0300 Subject: [PATCH 49/82] Move to use Solr joins instead. --- embargo.services.yml | 7 + .../EmbargoJoinProcessorEventSubscriber.php | 162 +++++++++++++++++ .../processor/EmbargoJoinProcessor.php | 170 ++++++++++++++++++ .../search_api/processor/EmbargoProcessor.php | 17 +- 4 files changed, 350 insertions(+), 6 deletions(-) create mode 100644 src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php create mode 100644 src/Plugin/search_api/processor/EmbargoJoinProcessor.php diff --git a/embargo.services.yml b/embargo.services.yml index e5acd21..9158cd4 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -43,3 +43,10 @@ services: 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' } diff --git a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php new file mode 100644 index 0000000..ad62a34 --- /dev/null +++ b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php @@ -0,0 +1,162 @@ +get('search_api.fields_helper'), + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('request_stack'), + ); + } + + /** + * {@inheritDoc} + */ + public static function getSubscribedEvents() { + return [ + SearchApiSolrEvents::PRE_QUERY => 'preQuery', + ]; + } + + /** + * 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 { + dsm('asdf'); + $search_api_query = $event->getSearchApiQuery(); + if (!$search_api_query->hasTag('embargo_join_processor')) { + return; + } + + $backend = $search_api_query->getIndex()->getServerInstance()->getBackend(); + assert($backend instanceof SolrBackendInterface); + $map = $backend->getSolrFieldNames($search_api_query->getIndex()); + $get_field_name = function (?string $datasource_id, string $property_path) use ($search_api_query, $map) { + $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); + + return $map[$field->getFieldIdentifier()]; + }; + + /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */ + $ip_range_entities = $this->entityTypeManager->getStorage('embargo_ip_range') + ->getApplicableIpRanges($this->requestStack->getCurrentRequest()->getClientIp()); + + $solarium_query = $event->getSolariumQuery(); + assert($solarium_query instanceof SolariumSelectQuery); + $helper = $solarium_query->getHelper(); + $filter_query = $solarium_query->createFilterQuery([ + 'key' => 'embargo_join', + 'query' => strtr( + implode(' ', [ + '(*:* -_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, 'embargo_node'), + $get_field_name('entity:embargo', 'embargoed_node:entity:nid'), + ), + '!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')), + ], + ), + ])->addTag('embargo_join_processor'); + + // {!tag=embargo:entity:media,embargo_schedule,embargo:entity:node,embargo_processor,embargo_access} + // ( + // +( + // (*:* -itm_id_1:[* TO *]) + // ( + // +(*:* -itm_expiration_type_1:"0") + // +itm_expiration_type_1:"1" + // +dm_expiration_date_1:[* TO "2024-04-05T00:00:00Z"] + // +(*:* -dm_expiration_date_1:["2024-04-06T00:00:00Z" TO "6596-12-04T00:00:00Z"]) + // ) + // itm_id_2:"1" + // ) + // +( + // (*:* -itm_id_3:[* TO *]) + // ( + // +(*:* -itm_expiration_type_2:"0") + // +itm_expiration_type_2:"1" + // +dm_expiration_date_2:[* TO "2024-04-05T00:00:00Z"] + // +(*:* -dm_expiration_date_2:["2024-04-06T00:00:00Z" TO "6596-12-04T00:00:00Z"]) + // ) + // itm_id_4:"1" + // ) + // ) + + + } + +} diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php new file mode 100644 index 0000000..a5fbbee --- /dev/null +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -0,0 +1,170 @@ +currentUser = $container->get('current_user'); + $instance->database = $container->get('database'); + + return $instance; + } + + /** + * {@inheritDoc} + */ + public static function supportsIndex(IndexInterface $index) { + return parent::supportsIndex($index) && in_array('entity:embargo', $index->getDatasourceIds()); + } + + /** + * {@inheritdoc} + */ + public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array { + $properties = []; + + if ($datasource === NULL) { + $properties['embargo_node'] = new ProcessorProperty([ + 'processor_id' => $this->getPluginId(), + 'is_list' => TRUE, + '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::ENTITY_TYPES)) { + return; + } + try { + $entity = $item->getOriginalObject()->getValue(); + } + catch (SearchApiException) { + return; + } + if (!($entity instanceof EntityInterface)) { + return; + } + + $embargo_node_fields = $this->getFieldsHelper()->filterForPropertyPath($item->getFields(FALSE), NULL, 'embargo_node'); + if ($embargo_node_fields) { + // Identify the nodes. + if ($entity->getEntityTypeId() === 'node') { + $nodes = [$entity->id()]; + } + else { + $column = match ($entity->getEntityTypeId()) { + 'media' => 'mid', + 'file' => 'fid', + }; + $nodes = array_unique( + $this->database->select(LUTGeneratorInterface::TABLE_NAME, 'lut') + ->fields('lut', ['nid']) + ->condition("lut.{$column}", $entity->id()) + ->execute() + ->fetchCol() + ); + } + + foreach ($embargo_node_fields as $field) { + foreach ($nodes as $node_id) { + $field->addValue($node_id); + } + } + } + + } + + /** + * {@inheritDoc} + */ + public function preIndexSave() : void { + parent::preIndexSave(); + + $this->ensureField(NULL, 'embargo_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; + } + + $query->addTag('embargo_join_processor'); + } + +} diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 82f9613..7b06f85 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -4,15 +4,18 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Cache\RefinableCacheableDependencyInterface; +use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\embargo\EmbargoInterface; use Drupal\embargo\Plugin\search_api\processor\Property\ListableEntityProcessorProperty; +use Drupal\islandora_hierarchical_access\LUTGeneratorInterface; use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; +use Drupal\search_api\Processor\ProcessorProperty; use Drupal\search_api\Query\ConditionGroupInterface; use Drupal\search_api\Query\QueryInterface; use Drupal\search_api\SearchApiException; @@ -87,15 +90,17 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritdoc} */ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : array { + $properties = []; + if ($datasource === NULL) { - return []; + return $properties; } - return [ - 'embargo' => ListableEntityProcessorProperty::create('embargo') - ->setList() - ->setProcessorId($this->getPluginId()), - ]; + $properties['embargo'] = ListableEntityProcessorProperty::create('embargo') + ->setList() + ->setProcessorId($this->getPluginId()); + + return $properties; } /** From 2360edb32195fc8d6c94ad81109b69fe17ae720d Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 9 Apr 2024 10:38:37 -0300 Subject: [PATCH 50/82] Rework over to make use of joins. --- .../EmbargoJoinProcessorEventSubscriber.php | 102 ++++++++++------- .../processor/EmbargoJoinProcessor.php | 108 +++++++++++++++++- 2 files changed, 162 insertions(+), 48 deletions(-) diff --git a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php index ad62a34..867de9e 100644 --- a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php +++ b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php @@ -5,6 +5,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\search_api\Query\QueryInterface; use Drupal\search_api\Utility\FieldsHelperInterface; use Drupal\search_api_solr\Event\PreQueryEvent; use Drupal\search_api_solr\Event\SearchApiSolrEvents; @@ -65,6 +66,10 @@ public function preQuery(PreQueryEvent $event) : void { return; } + $query_info = $search_api_query->getOption('embargo_join_processor'); + /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */ + $ip_range_entities = $query_info['ip_ranges']; + $backend = $search_api_query->getIndex()->getServerInstance()->getBackend(); assert($backend instanceof SolrBackendInterface); $map = $backend->getSolrFieldNames($search_api_query->getIndex()); @@ -80,57 +85,66 @@ public function preQuery(PreQueryEvent $event) : void { return $map[$field->getFieldIdentifier()]; }; - /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */ - $ip_range_entities = $this->entityTypeManager->getStorage('embargo_ip_range') - ->getApplicableIpRanges($this->requestStack->getCurrentRequest()->getClientIp()); - $solarium_query = $event->getSolariumQuery(); assert($solarium_query instanceof SolariumSelectQuery); $helper = $solarium_query->getHelper(); - $filter_query = $solarium_query->createFilterQuery([ - 'key' => 'embargo_join', - 'query' => strtr( - implode(' ', [ - '(*:* -_query_:"!join*:*")', - '_query_:"!join(', + + foreach ($query_info['queries'] as $type => $info) { + $solarium_query->createFilterQuery([ + 'key' => "embargo_join:{$type}", + 'query' => strtr( implode(' ', [ - '+(*:* -!type_field:\\"0\\")', - '+!type_field:\\"1\\"', - '+!date_field:[* TO \\"!date_value\\"]', - '+(*:* -!date_field:[\\"!next_date_value\\" TO *])', + '(*:* -!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!exempt_user_field:"!current_user"', - $ip_range_entities ? '_query_:"!join!exempt_ip_field:(!exempt_ip_ranges)"' : '', - ]), - [ - '!join' => $helper->join( - $get_field_name(NULL, 'embargo_node'), - $get_field_name('entity:embargo', 'embargoed_node:entity:nid'), - ), - '!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(...), + [ + '!join' => $helper->join( + $get_field_name(NULL, $info['path']), + $get_field_name('entity:embargo', 'embargoed_node:entity:nid'), + ), + '!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( - function ($range) { - return $range->id(); - }, - $ip_range_entities + $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')), - ], - ), - ])->addTag('embargo_join_processor'); + ), + '!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}"); + } // {!tag=embargo:entity:media,embargo_schedule,embargo:entity:node,embargo_processor,embargo_access} // ( diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index a5fbbee..ad55d34 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -5,8 +5,10 @@ use Drupal\Core\Cache\RefinableCacheableDependencyInterface; use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; +use Drupal\embargo\EmbargoInterface; use Drupal\islandora_hierarchical_access\LUTGeneratorInterface; use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\IndexInterface; @@ -16,6 +18,7 @@ use Drupal\search_api\Query\QueryInterface; use Drupal\search_api\SearchApiException; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\HttpFoundation\RequestStack; /** * A search_api processor to add embargo related info. @@ -23,8 +26,8 @@ * @SearchApiProcessor( * id = "embargo_join_processor", * label = @Translation("Embargo access, join-wise"), - * description = @Translation("Add information regarding embargo access constraints."), - * stages = { + * description = @Translation("Add information regarding embargo access + * constraints."), stages = { * "add_properties" = 20, * "pre_index_save" = 20, * "preprocess_query" = 20, @@ -36,6 +39,7 @@ class EmbargoJoinProcessor extends ProcessorPluginBase implements ContainerFactoryPluginInterface { const ENTITY_TYPES = ['file', 'media', 'node']; + const ALL_ENTITY_TYPES = ['file', 'media', 'node', 'embargo']; /** * The currently logged-in user. @@ -51,6 +55,10 @@ class EmbargoJoinProcessor extends ProcessorPluginBase implements ContainerFacto */ protected Connection $database; + protected EntityTypeManagerInterface $entityTypeManager; + + protected RequestStack $requestStack; + /** * {@inheritdoc} */ @@ -59,6 +67,8 @@ public static function create(ContainerInterface $container, array $configuratio $instance->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; } @@ -67,7 +77,14 @@ public static function create(ContainerInterface $container, array $configuratio * {@inheritDoc} */ public static function supportsIndex(IndexInterface $index) { - return parent::supportsIndex($index) && in_array('entity:embargo', $index->getDatasourceIds()); + 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) + ); } /** @@ -77,11 +94,24 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : $properties = []; if ($datasource === NULL) { + // Represent the node(s) to which a general content entity is associated. $properties['embargo_node'] = new ProcessorProperty([ 'processor_id' => $this->getPluginId(), 'is_list' => TRUE, 'is_computed' => TRUE, ]); + // Represent the node of which a "file" embargo is associated. + $properties['embargo_node__file'] = new ProcessorProperty([ + 'processor_id' => $this->getPluginId(), + 'is_list' => FALSE, + 'is_computed' => TRUE, + ]); + // Represent the node of which a "node" embargo is associated. + $properties['embargo_node__node'] = new ProcessorProperty([ + 'processor_id' => $this->getPluginId(), + 'is_list' => FALSE, + 'is_computed' => TRUE, + ]); } return $properties; @@ -95,7 +125,7 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : * @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)) { + if (!in_array($item->getDatasource()->getEntityTypeId(), static::ALL_ENTITY_TYPES)) { return; } try { @@ -108,6 +138,24 @@ public function addFieldValues(ItemInterface $item) : void { return; } + if (in_array($item->getDatasource()->getEntityTypeId(), static::ENTITY_TYPES)) { + $this->doAddNodeField($item, $entity); + } + else { + $this->doAddEmbargoField($item, $entity); + } + + } + + /** + * 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, 'embargo_node'); if ($embargo_node_fields) { // Identify the nodes. @@ -134,7 +182,30 @@ public function addFieldValues(ItemInterface $item) : void { } } } + } + + /** + * 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 => ['embargo_node__file', 'embargo_node__node'], + EmbargoInterface::EMBARGO_TYPE_NODE => ['embargo_node__node'], + }; + $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()); + } + } } /** @@ -144,6 +215,8 @@ public function preIndexSave() : void { parent::preIndexSave(); $this->ensureField(NULL, 'embargo_node', 'integer'); + $this->ensureField(NULL, 'embargo_node__file', 'integer'); + $this->ensureField(NULL, 'embargo_node__node', 'integer'); $this->ensureField('entity:embargo', 'id', 'integer'); $this->ensureField('entity:embargo', 'embargoed_node:entity:nid', 'integer'); @@ -164,7 +237,34 @@ public function preprocessSearchQuery(QueryInterface $query) : void { return; } + /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */ + $ip_range_entities = $this->entityTypeManager->getStorage('embargo_ip_range') + ->getApplicableIpRanges($this->requestStack->getCurrentRequest()->getClientIp()); + + $info = [ + 'ip_ranges' => $ip_range_entities, + 'queries' => [], + ]; + + if (in_array('entity:node', $this->index->getDatasourceIds())) { + $info['queries']['node'] = [ + 'data sources' => ['entity:node'], + 'path' => 'embargo_node__node', + ]; + } + if ($intersection = array_intersect($this->index->getDatasourceIds(), ['entity:media', 'entity:file'])) { + $info['queries']['file'] = [ + 'data sources' => $intersection, + 'path' => 'embargo_node__file', + ]; + } + + if (!$info['queries']) { + return; + } + $query->addTag('embargo_join_processor'); + $query->setOption('embargo_join_processor', $info); } } From 7e8d650be4bff1ec19d8a60babb7b7a83a774a5a Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 9 Apr 2024 11:07:04 -0300 Subject: [PATCH 51/82] Rework tracker logic slightly. --- .../EmbargoJoinProcessorEventSubscriber.php | 27 ------------ .../processor/EmbargoJoinProcessor.php | 4 ++ src/SearchApiTracker.php | 44 ++++++++++++++++++- 3 files changed, 46 insertions(+), 29 deletions(-) diff --git a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php index 867de9e..da5d9ad 100644 --- a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php +++ b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php @@ -5,7 +5,6 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\search_api\Query\QueryInterface; use Drupal\search_api\Utility\FieldsHelperInterface; use Drupal\search_api_solr\Event\PreQueryEvent; use Drupal\search_api_solr\Event\SearchApiSolrEvents; @@ -60,7 +59,6 @@ public static function getSubscribedEvents() { * The event to which to respond. */ public function preQuery(PreQueryEvent $event) : void { - dsm('asdf'); $search_api_query = $event->getSearchApiQuery(); if (!$search_api_query->hasTag('embargo_join_processor')) { return; @@ -146,31 +144,6 @@ function (string $source_id) { ])->addTag("embargo_join_processor:{$type}"); } - // {!tag=embargo:entity:media,embargo_schedule,embargo:entity:node,embargo_processor,embargo_access} - // ( - // +( - // (*:* -itm_id_1:[* TO *]) - // ( - // +(*:* -itm_expiration_type_1:"0") - // +itm_expiration_type_1:"1" - // +dm_expiration_date_1:[* TO "2024-04-05T00:00:00Z"] - // +(*:* -dm_expiration_date_1:["2024-04-06T00:00:00Z" TO "6596-12-04T00:00:00Z"]) - // ) - // itm_id_2:"1" - // ) - // +( - // (*:* -itm_id_3:[* TO *]) - // ( - // +(*:* -itm_expiration_type_2:"0") - // +itm_expiration_type_2:"1" - // +dm_expiration_date_2:[* TO "2024-04-05T00:00:00Z"] - // +(*:* -dm_expiration_date_2:["2024-04-06T00:00:00Z" TO "6596-12-04T00:00:00Z"]) - // ) - // itm_id_4:"1" - // ) - // ) - - } } diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index ad55d34..d2ddaa5 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -263,6 +263,10 @@ public function preprocessSearchQuery(QueryInterface $query) : void { return; } + $query->addCacheContexts([ + 'user', + ]); + $query->addTag('embargo_join_processor'); $query->setOption('embargo_join_processor', $info); } diff --git a/src/SearchApiTracker.php b/src/SearchApiTracker.php index cbd7298..995fabf 100644 --- a/src/SearchApiTracker.php +++ b/src/SearchApiTracker.php @@ -49,6 +49,43 @@ public static function create(ContainerInterface $container) : self { ); } + /** + * 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. * @@ -57,7 +94,7 @@ public static function create(ContainerInterface $container) : self { */ public function track(EntityInterface $entity) : void { assert($entity instanceof EmbargoInterface); - if (!$this->moduleHandler->moduleExists('search_api')) { + if (!$this->isProcessorEnabled()) { return; } @@ -84,7 +121,7 @@ public function track(EntityInterface $entity) : void { * The entity to track. */ public function doTrack(ContentEntityInterface $entity) : void { - if (!$this->moduleHandler->moduleExists('search_api')) { + if (!$this->isProcessorEnabled()) { return; } $this->trackingManager->trackEntityChange($entity); @@ -144,6 +181,9 @@ protected function getMediaType(MediaInterface $media) : MediaTypeInterface { * 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; From dd176013095024ecdd1c8d0753d7407f514f1bf8 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 9 Apr 2024 11:08:40 -0300 Subject: [PATCH 52/82] Coding standards. --- .../search_api/processor/EmbargoJoinProcessor.php | 10 ++++++++++ src/Plugin/search_api/processor/EmbargoProcessor.php | 3 --- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index d2ddaa5..bd95aac 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -55,8 +55,18 @@ class EmbargoJoinProcessor extends ProcessorPluginBase implements ContainerFacto */ protected Connection $database; + /** + * Drupal's entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ protected EntityTypeManagerInterface $entityTypeManager; + /** + * Symfony's request stack info. + * + * @var \Symfony\Component\HttpFoundation\RequestStack + */ protected RequestStack $requestStack; /** diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index 7b06f85..116d4be 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -4,18 +4,15 @@ use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Cache\RefinableCacheableDependencyInterface; -use Drupal\Core\Database\Connection; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\embargo\EmbargoInterface; use Drupal\embargo\Plugin\search_api\processor\Property\ListableEntityProcessorProperty; -use Drupal\islandora_hierarchical_access\LUTGeneratorInterface; use Drupal\search_api\Datasource\DatasourceInterface; use Drupal\search_api\Item\ItemInterface; use Drupal\search_api\Processor\ProcessorPluginBase; -use Drupal\search_api\Processor\ProcessorProperty; use Drupal\search_api\Query\ConditionGroupInterface; use Drupal\search_api\Query\QueryInterface; use Drupal\search_api\SearchApiException; From 039969e2a31faaf58ba7dd69210aacef602315f6 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 9 Apr 2024 11:54:04 -0300 Subject: [PATCH 53/82] Fix up the direction of multiplicity with the values. --- .../EmbargoJoinProcessorEventSubscriber.php | 24 ++++++++++++------- .../processor/EmbargoJoinProcessor.php | 14 +++++++++-- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php index da5d9ad..d97bd25 100644 --- a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php +++ b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php @@ -71,16 +71,22 @@ public function preQuery(PreQueryEvent $event) : void { $backend = $search_api_query->getIndex()->getServerInstance()->getBackend(); assert($backend instanceof SolrBackendInterface); $map = $backend->getSolrFieldNames($search_api_query->getIndex()); - $get_field_name = function (?string $datasource_id, string $property_path) use ($search_api_query, $map) { - $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 = []; + $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); - return $map[$field->getFieldIdentifier()]; + $memoized_map[$key] = $map[$field->getFieldIdentifier()]; + } + + return $memoized_map[$key]; }; $solarium_query = $event->getSolariumQuery(); diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index bd95aac..b9d1bf6 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -205,8 +205,8 @@ protected function doAddNodeField(ItemInterface $item, EntityInterface $entity) protected function doAddEmbargoField(ItemInterface $item, EntityInterface $entity) : void { assert($entity instanceof EmbargoInterface); $paths = match ($entity->getEmbargoType()) { - EmbargoInterface::EMBARGO_TYPE_FILE => ['embargo_node__file', 'embargo_node__node'], - EmbargoInterface::EMBARGO_TYPE_NODE => ['embargo_node__node'], + EmbargoInterface::EMBARGO_TYPE_FILE => ['embargo_node__file'], + EmbargoInterface::EMBARGO_TYPE_NODE => ['embargo_node__node', 'embargo_node__file'], }; $fields = $item->getFields(FALSE); @@ -274,8 +274,18 @@ public function preprocessSearchQuery(QueryInterface $query) : void { } $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 dates deal with granularity to the day. + $query->mergeCacheMaxAge(24 * 3600); + + /** @var \Drupal\Core\Entity\EntityTypeInterface $embargo_type */ + $embargo_type = $this->entityTypeManager->getDefinition('embargo'); + $query->addCacheTags($embargo_type->getListCacheTags()); $query->addTag('embargo_join_processor'); $query->setOption('embargo_join_processor', $info); From 26e0b9032023f706e0ca4cf1cc00e049466a2673 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 9 Apr 2024 12:01:28 -0300 Subject: [PATCH 54/82] Add in the fix to the old processor. --- src/Plugin/search_api/processor/EmbargoProcessor.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index ca00306..c20edf9 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -137,8 +137,17 @@ public function addFieldValues(ItemInterface $item) : void { /** @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 ($embargoes as $embargo) { + foreach ($relevant_embargoes as $embargo) { $this->getFieldsHelper()->extractFields($embargo->getTypedData(), $to_extract); } From 7b226fd244639ead3f2546509ba161886fcd82b3 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 9 Apr 2024 12:08:24 -0300 Subject: [PATCH 55/82] Attempt to soften the dependency. --- .../EmbargoJoinProcessorEventSubscriber.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php index d97bd25..11de06c 100644 --- a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php +++ b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php @@ -47,9 +47,15 @@ public static function create(ContainerInterface $container) : self { * {@inheritDoc} */ public static function getSubscribedEvents() { - return [ - SearchApiSolrEvents::PRE_QUERY => 'preQuery', - ]; + $events = []; + + if (class_exists(SearchApiSolrEvents::class)) { + $events += [ + SearchApiSolrEvents::PRE_QUERY => 'preQuery', + ]; + } + + return $events; } /** From 87f1cbde2a1acb48e8dbad440057c262295d9acf Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 9 Apr 2024 13:17:16 -0300 Subject: [PATCH 56/82] Move calculation of current IP ranges. --- .../processor/EmbargoJoinProcessor.php | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index b9d1bf6..e8d8a18 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -247,12 +247,7 @@ public function preprocessSearchQuery(QueryInterface $query) : void { return; } - /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */ - $ip_range_entities = $this->entityTypeManager->getStorage('embargo_ip_range') - ->getApplicableIpRanges($this->requestStack->getCurrentRequest()->getClientIp()); - $info = [ - 'ip_ranges' => $ip_range_entities, 'queries' => [], ]; @@ -273,6 +268,11 @@ public function preprocessSearchQuery(QueryInterface $query) : void { return; } + /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */ + $ip_range_entities = $this->entityTypeManager->getStorage('embargo_ip_range') + ->getApplicableIpRanges($this->requestStack->getCurrentRequest()->getClientIp()); + $info['ip_ranges'] = $ip_range_entities; + $query->addCacheContexts([ // Caching by groups of ranges instead of individually should promote // cacheability. @@ -283,9 +283,12 @@ public function preprocessSearchQuery(QueryInterface $query) : void { // Embargo dates deal with granularity to the day. $query->mergeCacheMaxAge(24 * 3600); - /** @var \Drupal\Core\Entity\EntityTypeInterface $embargo_type */ - $embargo_type = $this->entityTypeManager->getDefinition('embargo'); - $query->addCacheTags($embargo_type->getListCacheTags()); + $types = ['embargo', 'embargo_ip_range']; + 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', $info); From f89a4c9d318fff3ab69c20c16714441ac5beec05 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 9 Apr 2024 13:30:56 -0300 Subject: [PATCH 57/82] Pull "info" apart to separate settings. --- .../EmbargoJoinProcessorEventSubscriber.php | 13 +++++++++---- .../search_api/processor/EmbargoJoinProcessor.php | 14 ++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php index 11de06c..64290cd 100644 --- a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php +++ b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php @@ -70,9 +70,11 @@ public function preQuery(PreQueryEvent $event) : void { return; } - $query_info = $search_api_query->getOption('embargo_join_processor'); - /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */ - $ip_range_entities = $query_info['ip_ranges']; + $queries = $search_api_query->getOption('embargo_join_processor__queries', []); + + if (!$queries) { + return; + } $backend = $search_api_query->getIndex()->getServerInstance()->getBackend(); assert($backend instanceof SolrBackendInterface); @@ -99,7 +101,10 @@ public function preQuery(PreQueryEvent $event) : void { assert($solarium_query instanceof SolariumSelectQuery); $helper = $solarium_query->getHelper(); - foreach ($query_info['queries'] as $type => $info) { + /** @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( diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index e8d8a18..f7109d9 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -247,31 +247,28 @@ public function preprocessSearchQuery(QueryInterface $query) : void { return; } - $info = [ - 'queries' => [], - ]; + $queries = []; if (in_array('entity:node', $this->index->getDatasourceIds())) { - $info['queries']['node'] = [ + $queries['node'] = [ 'data sources' => ['entity:node'], 'path' => 'embargo_node__node', ]; } if ($intersection = array_intersect($this->index->getDatasourceIds(), ['entity:media', 'entity:file'])) { - $info['queries']['file'] = [ + $queries['file'] = [ 'data sources' => $intersection, 'path' => 'embargo_node__file', ]; } - if (!$info['queries']) { + if (!$queries) { return; } /** @var \Drupal\embargo\IpRangeInterface[] $ip_range_entities */ $ip_range_entities = $this->entityTypeManager->getStorage('embargo_ip_range') ->getApplicableIpRanges($this->requestStack->getCurrentRequest()->getClientIp()); - $info['ip_ranges'] = $ip_range_entities; $query->addCacheContexts([ // Caching by groups of ranges instead of individually should promote @@ -291,7 +288,8 @@ public function preprocessSearchQuery(QueryInterface $query) : void { } $query->addTag('embargo_join_processor'); - $query->setOption('embargo_join_processor', $info); + $query->setOption('embargo_join_processor__ip_ranges', $ip_range_entities); + $query->setOption('embargo_join_processor__queries', $queries); } } From 3d119697ebed2b9f79a77b52cbf6d41a7b44e3de Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 10 Apr 2024 11:42:53 -0300 Subject: [PATCH 58/82] Adjustments. --- src/EmbargoStorageTrait.php | 8 ++++---- .../EmbargoJoinProcessorEventSubscriber.php | 4 ++-- .../search_api/processor/EmbargoJoinProcessor.php | 10 ++++++---- src/Plugin/search_api/processor/EmbargoProcessor.php | 5 ++--- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/EmbargoStorageTrait.php b/src/EmbargoStorageTrait.php index f8292ce..ef640ff 100644 --- a/src/EmbargoStorageTrait.php +++ b/src/EmbargoStorageTrait.php @@ -21,9 +21,9 @@ trait EmbargoStorageTrait { /** * The current request. * - * @var \Symfony\Component\HttpFoundation\Request + * @var \Symfony\Component\HttpFoundation\Request|null */ - protected Request $request; + protected ?Request $request; /** * The current user. @@ -95,13 +95,13 @@ protected function setUser(AccountInterface $user) : self { /** * The request visible to the trait. * - * @param \Symfony\Component\HttpFoundation\Request $request + * @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 { + protected function setRequest(?Request $request) : self { $this->request = $request; return $this; } diff --git a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php index 64290cd..565db11 100644 --- a/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php +++ b/src/EventSubscriber/EmbargoJoinProcessorEventSubscriber.php @@ -124,8 +124,8 @@ public function preQuery(PreQueryEvent $event) : void { ]), [ '!join' => $helper->join( - $get_field_name(NULL, $info['path']), - $get_field_name('entity:embargo', 'embargoed_node:entity:nid'), + $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'), diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index f7109d9..5dc4f12 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -26,8 +26,8 @@ * @SearchApiProcessor( * id = "embargo_join_processor", * label = @Translation("Embargo access, join-wise"), - * description = @Translation("Add information regarding embargo access - * constraints."), stages = { + * description = @Translation("Add information regarding embargo access constraints."), + * stages = { * "add_properties" = 20, * "pre_index_save" = 20, * "preprocess_query" = 20, @@ -252,13 +252,15 @@ public function preprocessSearchQuery(QueryInterface $query) : void { if (in_array('entity:node', $this->index->getDatasourceIds())) { $queries['node'] = [ 'data sources' => ['entity:node'], - 'path' => 'embargo_node__node', + 'embargo path' => 'embargo_node__node', + 'node path' => 'embargo_node', ]; } if ($intersection = array_intersect($this->index->getDatasourceIds(), ['entity:media', 'entity:file'])) { $queries['file'] = [ 'data sources' => $intersection, - 'path' => 'embargo_node__file', + 'embargo path' => 'embargo_node__file', + 'node path' => 'embargo_node', ]; } diff --git a/src/Plugin/search_api/processor/EmbargoProcessor.php b/src/Plugin/search_api/processor/EmbargoProcessor.php index c20edf9..278f96f 100644 --- a/src/Plugin/search_api/processor/EmbargoProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoProcessor.php @@ -25,9 +25,8 @@ * * @SearchApiProcessor( * id = "embargo_processor", - * label = @Translation("Embargo access"), - * description = @Translation("Add information regarding embargo access - * constraints."), + * label = @Translation("Embargo access (deprecated)"), + * description = @Translation("Add information regarding embargo access constraints."), * stages = { * "add_properties" = 20, * "pre_index_save" = 20, From 1e2a77cc2fa9ef153bc1de30c7fd45b6027e7616 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 10 Apr 2024 13:26:10 -0300 Subject: [PATCH 59/82] Carry-through more of constant use. --- .../processor/EmbargoJoinProcessor.php | 84 +++++++++++-------- 1 file changed, 48 insertions(+), 36 deletions(-) diff --git a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index 5dc4f12..90b8d3f 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -28,9 +28,9 @@ * label = @Translation("Embargo access, join-wise"), * description = @Translation("Add information regarding embargo access constraints."), * stages = { - * "add_properties" = 20, - * "pre_index_save" = 20, - * "preprocess_query" = 20, + * "add_properties" = 0, + * "pre_index_save" = 0, + * "preprocess_query" = 0, * }, * locked = false, * hidden = false, @@ -41,6 +41,10 @@ class EmbargoJoinProcessor extends ProcessorPluginBase implements ContainerFacto const ENTITY_TYPES = ['file', 'media', 'node']; const ALL_ENTITY_TYPES = ['file', 'media', 'node', 'embargo']; + const NODE_FIELD = 'embargo__node'; + const EMBARGO_FIELD_NODE = 'embargo__node__node'; + const EMBARGO_FIELD_FILE = 'embargo__node__file'; + /** * The currently logged-in user. * @@ -105,19 +109,19 @@ public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) : if ($datasource === NULL) { // Represent the node(s) to which a general content entity is associated. - $properties['embargo_node'] = new ProcessorProperty([ + $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['embargo_node__file'] = new ProcessorProperty([ + $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['embargo_node__node'] = new ProcessorProperty([ + $properties[static::EMBARGO_FIELD_NODE] = new ProcessorProperty([ 'processor_id' => $this->getPluginId(), 'is_list' => FALSE, 'is_computed' => TRUE, @@ -157,6 +161,32 @@ public function addFieldValues(ItemInterface $item) : void { } + /** + * 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. * @@ -166,25 +196,9 @@ public function addFieldValues(ItemInterface $item) : void { * 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, 'embargo_node'); + $embargo_node_fields = $this->getFieldsHelper()->filterForPropertyPath($item->getFields(FALSE), NULL, static::NODE_FIELD); if ($embargo_node_fields) { - // Identify the nodes. - if ($entity->getEntityTypeId() === 'node') { - $nodes = [$entity->id()]; - } - else { - $column = match ($entity->getEntityTypeId()) { - 'media' => 'mid', - 'file' => 'fid', - }; - $nodes = array_unique( - $this->database->select(LUTGeneratorInterface::TABLE_NAME, 'lut') - ->fields('lut', ['nid']) - ->condition("lut.{$column}", $entity->id()) - ->execute() - ->fetchCol() - ); - } + $nodes = array_unique($this->findRelatedNodes($entity)); foreach ($embargo_node_fields as $field) { foreach ($nodes as $node_id) { @@ -205,8 +219,8 @@ protected function doAddNodeField(ItemInterface $item, EntityInterface $entity) protected function doAddEmbargoField(ItemInterface $item, EntityInterface $entity) : void { assert($entity instanceof EmbargoInterface); $paths = match ($entity->getEmbargoType()) { - EmbargoInterface::EMBARGO_TYPE_FILE => ['embargo_node__file'], - EmbargoInterface::EMBARGO_TYPE_NODE => ['embargo_node__node', 'embargo_node__file'], + EmbargoInterface::EMBARGO_TYPE_FILE => [static::EMBARGO_FIELD_FILE], + EmbargoInterface::EMBARGO_TYPE_NODE => [static::EMBARGO_FIELD_NODE, static::EMBARGO_FIELD_FILE], }; $fields = $item->getFields(FALSE); @@ -222,11 +236,9 @@ protected function doAddEmbargoField(ItemInterface $item, EntityInterface $entit * {@inheritDoc} */ public function preIndexSave() : void { - parent::preIndexSave(); - - $this->ensureField(NULL, 'embargo_node', 'integer'); - $this->ensureField(NULL, 'embargo_node__file', 'integer'); - $this->ensureField(NULL, 'embargo_node__node', 'integer'); + $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'); @@ -252,15 +264,15 @@ public function preprocessSearchQuery(QueryInterface $query) : void { if (in_array('entity:node', $this->index->getDatasourceIds())) { $queries['node'] = [ 'data sources' => ['entity:node'], - 'embargo path' => 'embargo_node__node', - 'node path' => 'embargo_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' => 'embargo_node__file', - 'node path' => 'embargo_node', + 'embargo path' => static::EMBARGO_FIELD_FILE, + 'node path' => static::NODE_FIELD, ]; } @@ -282,7 +294,7 @@ public function preprocessSearchQuery(QueryInterface $query) : void { // Embargo dates deal with granularity to the day. $query->mergeCacheMaxAge(24 * 3600); - $types = ['embargo', 'embargo_ip_range']; + $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); From 49ac2b02907a7678b670542d340b1bdc28418415 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 10 Apr 2024 17:32:18 -0300 Subject: [PATCH 60/82] Pull the conditions we need to alter to deal with collections out. Should facilitate altering them to add additional conditions. ... the question now: Move to event dispatch to build? --- src/EmbargoExistenceQueryTrait.php | 62 +++++++++++++++++++++++++----- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php index de6b77c..cf7f84e 100644 --- a/src/EmbargoExistenceQueryTrait.php +++ b/src/EmbargoExistenceQueryTrait.php @@ -80,6 +80,49 @@ protected function applyExistenceQuery( ); } + /** + * 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 { + $condition = $query->orConditionGroup(); + + $embargo_alias = $query->getMetaData('embargo_alias'); + $target_aliases = $query->getMetaData('embargo_target_aliases'); + + $condition->where(strtr('!field IN (!targets)', [ + '!field' => "{$embargo_alias}.embargoed_node", + '!targets' => implode(', ', $target_aliases), + ])); + + return $condition; + } + + /** + * 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 { + $condition = $query->orConditionGroup(); + + $embargo_alias = $query->getMetaData('embargo_alias'); + $unexpired_alias = $query->getMetaData('embargo_unexpired_alias'); + + $condition->where("{$unexpired_alias}.embargoed_node = {$embargo_alias}.embargoed_node"); + + return $condition; + } + /** * Get query for negative assertion. * @@ -95,11 +138,10 @@ protected function getNullQuery(array $target_aliases, array $embargo_types) : S $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->where(strtr('!field IN (!targets)', [ - '!field' => "{$embargo_alias}.embargoed_node", - '!targets' => implode(', ', $target_aliases), - ])); + $query->condition($this->buildInclusionBaseCondition($query)); $query->condition("{$embargo_alias}.embargo_type", $embargo_types, 'IN'); return $query; @@ -123,15 +165,12 @@ protected function getAccessibleEmbargoesQuery(array $target_aliases, array $emb $embargo_existence->addExpression(1, 'embargo_allowed'); $embargo_existence->addMetaData('embargo_alias', $embargo_alias); + $embargo_existence->addMetaData('embargo_target_aliases', $target_aliases); - $replacements = [ - '!field' => "{$embargo_alias}.embargoed_node", - '!targets' => implode(', ', $target_aliases), - ]; $embargo_existence->condition( $embargo_existence->orConditionGroup() ->condition($existence_condition = $embargo_existence->andConditionGroup() - ->where(strtr('!field IN (!targets)', $replacements)) + ->condition($this->buildInclusionBaseCondition($embargo_existence)) ->condition($embargo_or = $embargo_existence->orConditionGroup()) ) ); @@ -159,7 +198,10 @@ protected function getAccessibleEmbargoesQuery(array $target_aliases, array $emb $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') - ->where("ue.embargoed_node = {$embargo_alias}.embargoed_node") + ->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() From a323d51a40f82973d35d0eab1eac5069443d623e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 11 Apr 2024 10:56:21 -0300 Subject: [PATCH 61/82] Move over to event handling. --- embargo.services.yml | 10 ++- src/Access/QueryTagger.php | 5 +- src/EmbargoExistenceQueryTrait.php | 55 ++++++++++----- src/Event/AbstractTagEvent.php | 70 +++++++++++++++++++ src/Event/EmbargoEvents.php | 14 ++++ src/Event/TagExclusionEvent.php | 20 ++++++ src/Event/TagInclusionEvent.php | 10 +++ ...ndoraHierarchicalAccessEventSubscriber.php | 5 +- .../TaggingEventSubscriber.php | 48 +++++++++++++ 9 files changed, 215 insertions(+), 22 deletions(-) create mode 100644 src/Event/AbstractTagEvent.php create mode 100644 src/Event/EmbargoEvents.php create mode 100644 src/Event/TagExclusionEvent.php create mode 100644 src/Event/TagInclusionEvent.php create mode 100644 src/EventSubscriber/TaggingEventSubscriber.php diff --git a/embargo.services.yml b/embargo.services.yml index fff9bb0..d5a6f3c 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: @@ -56,4 +57,9 @@ services: - '@request_stack' - '@entity_type.manager' tags: - - { name: cache.context } + - { name: 'cache.context' } + embargo.tagging_event_subscriber: + class: Drupal\embargo\EventSubscriber\TaggingEventSubscriber + tags: + - { name: 'event_subscriber' } + diff --git a/src/Access/QueryTagger.php b/src/Access/QueryTagger.php index 202c158..c5b0189 100644 --- a/src/Access/QueryTagger.php +++ b/src/Access/QueryTagger.php @@ -13,6 +13,7 @@ use Drupal\islandora_hierarchical_access\Access\QueryConjunctionTrait; 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. @@ -32,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(); @@ -40,6 +42,7 @@ public function __construct( $this->entityTypeManager = $entity_type_manager; $this->time = $time; $this->dateFormatter = $date_formatter; + $this->setEventDispatcher($event_dispatcher); } /** diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php index cf7f84e..18db51b 100644 --- a/src/EmbargoExistenceQueryTrait.php +++ b/src/EmbargoExistenceQueryTrait.php @@ -10,6 +10,9 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; +use Drupal\embargo\Event\TagExclusionEvent; +use Drupal\embargo\Event\TagInclusionEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; /** * Helper trait; facilitate filtering of embargoed entities. @@ -58,6 +61,13 @@ trait EmbargoExistenceQueryTrait { */ protected DateFormatterInterface $dateFormatter; + /** + * The event dispatcher service. + * + * @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface + */ + protected EventDispatcherInterface $eventDispatcher; + /** * Helper; apply existence checks to a node(-like) table. * @@ -80,6 +90,30 @@ protected function applyExistenceQuery( ); } + /** + * 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. * @@ -90,17 +124,9 @@ protected function applyExistenceQuery( * The condition to attach. */ protected function buildInclusionBaseCondition(SelectInterface $query) : ConditionInterface { - $condition = $query->orConditionGroup(); + $dispatched_event = $this->getEventDispatch()->dispatch(new TagInclusionEvent($query)); - $embargo_alias = $query->getMetaData('embargo_alias'); - $target_aliases = $query->getMetaData('embargo_target_aliases'); - - $condition->where(strtr('!field IN (!targets)', [ - '!field' => "{$embargo_alias}.embargoed_node", - '!targets' => implode(', ', $target_aliases), - ])); - - return $condition; + return $dispatched_event->getCondition(); } /** @@ -113,14 +139,9 @@ protected function buildInclusionBaseCondition(SelectInterface $query) : Conditi * The condition to attach. */ protected function buildExclusionBaseCondition(SelectInterface $query) : ConditionInterface { - $condition = $query->orConditionGroup(); - - $embargo_alias = $query->getMetaData('embargo_alias'); - $unexpired_alias = $query->getMetaData('embargo_unexpired_alias'); - - $condition->where("{$unexpired_alias}.embargoed_node = {$embargo_alias}.embargoed_node"); + $dispatched_event = $this->getEventDispatch()->dispatch(new TagExclusionEvent($query)); - return $condition; + return $dispatched_event->getCondition(); } /** 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('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')); } /** diff --git a/src/EventSubscriber/TaggingEventSubscriber.php b/src/EventSubscriber/TaggingEventSubscriber.php new file mode 100644 index 0000000..8d86a1e --- /dev/null +++ b/src/EventSubscriber/TaggingEventSubscriber.php @@ -0,0 +1,48 @@ + '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 { + $event->getCondition()->where("{$event->getUnexpiredAlias()}.embargoed_node = {$event->getEmbargoAlias()}.embargoed_node"); + } + +} From e036ba627d2c640b27c2cb5b086c75b158d3d207 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 11 Apr 2024 11:06:00 -0300 Subject: [PATCH 62/82] Drop extra newline. --- embargo.services.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/embargo.services.yml b/embargo.services.yml index d5a6f3c..c6ef73b 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -62,4 +62,3 @@ services: class: Drupal\embargo\EventSubscriber\TaggingEventSubscriber tags: - { name: 'event_subscriber' } - From ad63a229a825227255b9a46f0683ce2d51253517 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 11 Apr 2024 11:34:13 -0300 Subject: [PATCH 63/82] Add something of a comment. --- src/EventSubscriber/TaggingEventSubscriber.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/EventSubscriber/TaggingEventSubscriber.php b/src/EventSubscriber/TaggingEventSubscriber.php index 8d86a1e..0044ca5 100644 --- a/src/EventSubscriber/TaggingEventSubscriber.php +++ b/src/EventSubscriber/TaggingEventSubscriber.php @@ -42,6 +42,9 @@ public function inclusion(TagInclusionEvent $event) : void { * 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"); } From f29f33fc0ab12a93bedd12b218c37f07ffe43e2e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 11 Apr 2024 14:41:36 -0300 Subject: [PATCH 64/82] Slight refactor. Allow overriding of entity setup, looking to move embargoes set up the tree. --- tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php index 82c912d..e1d3703 100644 --- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php +++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php @@ -15,6 +15,7 @@ * @group embargo */ class EmbargoAccessQueryTaggingAlterTest extends EmbargoKernelTestBase { + use DatabaseQueryTestTraits; /** @@ -101,6 +102,13 @@ class EmbargoAccessQueryTaggingAlterTest extends EmbargoKernelTestBase { 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. $this->embargoedNode = $this->createNode(); $this->embargoedMedia = $this->createMedia($this->embargoedFile = $this->createFile(), $this->embargoedNode); From 8a4783ec2e21a25be79fedd63aff3a7639467e04 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 11 Apr 2024 15:30:16 -0300 Subject: [PATCH 65/82] Document our processors. --- README.md | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 06eef40..f23590f 100644 --- a/README.md +++ b/README.md @@ -19,25 +19,45 @@ 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. + +- Embargoed items show up in search results: Enable one of our `search_api` processors to handle applying embargo restrictions. ## Troubleshooting/Issues From 3c3f984e9e6e0feae15fcf96b22afae74e5eb559 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 11 Apr 2024 15:32:16 -0300 Subject: [PATCH 66/82] Bit more docs. --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f23590f..aab5cab 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,14 @@ An embargo can be applied to an existing node by clicking the `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 show up in search results: Enable one of our `search_api` processors to handle applying embargo restrictions. +## 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 From 5b8e91eeaf2281f43732374aa8883eacf5182d92 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 15 Apr 2024 15:32:51 -0300 Subject: [PATCH 67/82] Slap together minimal views integration. --- src/EmbargoIpRangeViewsData.php | 12 ++++++++++++ src/EmbargoViewsData.php | 12 ++++++++++++ src/Entity/Embargo.php | 1 + src/Entity/IpRange.php | 1 + 4 files changed, 26 insertions(+) create mode 100644 src/EmbargoIpRangeViewsData.php create mode 100644 src/EmbargoViewsData.php 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 @@ + Date: Mon, 15 Apr 2024 16:52:51 -0300 Subject: [PATCH 68/82] Base "reverse" integration. Could probably use a better name... --- embargo.views.inc | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 embargo.views.inc diff --git a/embargo.views.inc b/embargo.views.inc new file mode 100644 index 0000000..2d9e5ea --- /dev/null +++ b/embargo.views.inc @@ -0,0 +1,24 @@ + \t('Embargoed Nodee'), + 'relationship' => [ + 'base' => 'embargo', + 'base field' => 'embargoed_node', + 'field' => 'nid', + 'id' => 'standard', + 'label' => \t('Embargoed Nodee'), + ], + ]; + +} From 66217cb2f57403a6fd1717411fff76064e3d1ebe Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 15 Apr 2024 17:04:57 -0300 Subject: [PATCH 69/82] Rename the altered-in relationship field. --- embargo.views.inc | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/embargo.views.inc b/embargo.views.inc index 2d9e5ea..d103c82 100644 --- a/embargo.views.inc +++ b/embargo.views.inc @@ -10,14 +10,15 @@ */ function embargo_views_data_alter(array &$data) { - $data['node_field_data']['embargoed_node'] = [ - 'title' => \t('Embargoed Nodee'), + $data['node_field_data']['embargo__embargoes'] = [ + 'title' => \t('Embargoes'), + 'help' => \t('Embargoes applicable to the given node.'), 'relationship' => [ 'base' => 'embargo', 'base field' => 'embargoed_node', 'field' => 'nid', 'id' => 'standard', - 'label' => \t('Embargoed Nodee'), + 'label' => \t('Embargoes'), ], ]; From 7d9c78f46d766a2987edc164a38ccdb4030a30ec Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Apr 2024 12:01:03 -0300 Subject: [PATCH 70/82] Fix rendering of embargo date. --- src/Plugin/Block/EmbargoNotificationBlock.php | 5 ++++- templates/embargo-notification.html.twig | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Plugin/Block/EmbargoNotificationBlock.php b/src/Plugin/Block/EmbargoNotificationBlock.php index 7ad8d4d..c7835a0 100644 --- a/src/Plugin/Block/EmbargoNotificationBlock.php +++ b/src/Plugin/Block/EmbargoNotificationBlock.php @@ -151,7 +151,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 +171,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 @@ -202,7 +205,7 @@ public function getCacheTags() { */ public function getCacheContexts() { // Ensure that with every new node/route, this block will be rebuilt. - return Cache::mergeContexts(parent::getCacheContexts(), ['route']); + return Cache::mergeContexts(parent::getCacheContexts(), ['route', 'url']); } } 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 From cd08535b20100ec6be5c145f3e09e8b5c2d6525a Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Apr 2024 14:00:08 -0300 Subject: [PATCH 71/82] Slight bit of reworking. Block construction and type-hinting. --- src/Plugin/Block/EmbargoNotificationBlock.php | 139 +++++++++--------- 1 file changed, 66 insertions(+), 73 deletions(-) diff --git a/src/Plugin/Block/EmbargoNotificationBlock.php b/src/Plugin/Block/EmbargoNotificationBlock.php index c7835a0..e4f0080 100644 --- a/src/Plugin/Block/EmbargoNotificationBlock.php +++ b/src/Plugin/Block/EmbargoNotificationBlock.php @@ -5,7 +5,6 @@ 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; @@ -14,7 +13,7 @@ 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. @@ -32,114 +31,79 @@ class EmbargoNotificationBlock extends BlockBase implements ContainerFactoryPlug * * @var string */ - 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 */ - protected $user; + protected AccountProxyInterface $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 []; } @@ -187,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', 'url']); + 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; } } From 79c87de38200f75a868ac029b1348bd73e9263af Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Apr 2024 14:16:51 -0300 Subject: [PATCH 72/82] Allow for the nullable addresses. --- src/Plugin/Block/EmbargoNotificationBlock.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Plugin/Block/EmbargoNotificationBlock.php b/src/Plugin/Block/EmbargoNotificationBlock.php index e4f0080..2cdfe0d 100644 --- a/src/Plugin/Block/EmbargoNotificationBlock.php +++ b/src/Plugin/Block/EmbargoNotificationBlock.php @@ -29,9 +29,9 @@ class EmbargoNotificationBlock extends BlockBase implements ContainerFactoryPlug /** * The admin email address. * - * @var string + * @var string|null */ - protected string $adminMail; + protected ?string $adminMail; /** * The notification message. From 80aa0a767f997cd41a0ff69a87c601fc315184c0 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Apr 2024 14:19:06 -0300 Subject: [PATCH 73/82] Wider interface. --- src/Plugin/Block/EmbargoNotificationBlock.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Plugin/Block/EmbargoNotificationBlock.php b/src/Plugin/Block/EmbargoNotificationBlock.php index 2cdfe0d..e4d3238 100644 --- a/src/Plugin/Block/EmbargoNotificationBlock.php +++ b/src/Plugin/Block/EmbargoNotificationBlock.php @@ -9,6 +9,7 @@ use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\ResettableStackedRouteMatchInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\embargo\EmbargoInterface; use Drupal\node\NodeInterface; @@ -57,9 +58,9 @@ class EmbargoNotificationBlock extends BlockBase implements ContainerFactoryPlug /** * The current user. * - * @var \Drupal\Core\Session\AccountProxyInterface + * @var \Drupal\Core\Session\AccountInterface */ - protected AccountProxyInterface $user; + protected AccountInterface $user; /** * The object renderer. From 383b510547d4c8d906ff1bf7aa632df33b563be9 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Apr 2024 14:24:51 -0300 Subject: [PATCH 74/82] Remove unused use. --- src/Plugin/Block/EmbargoNotificationBlock.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Plugin/Block/EmbargoNotificationBlock.php b/src/Plugin/Block/EmbargoNotificationBlock.php index e4d3238..10e5eb1 100644 --- a/src/Plugin/Block/EmbargoNotificationBlock.php +++ b/src/Plugin/Block/EmbargoNotificationBlock.php @@ -10,7 +10,6 @@ use Drupal\Core\Render\RendererInterface; use Drupal\Core\Routing\ResettableStackedRouteMatchInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Session\AccountProxyInterface; use Drupal\embargo\EmbargoInterface; use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; From 7ab191b09418cba1f6a7a33cda65006b24fbb0db Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 16 Apr 2024 15:57:59 -0300 Subject: [PATCH 75/82] Fix up user relationship. --- embargo.views.inc | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/embargo.views.inc b/embargo.views.inc index d103c82..5128bc5 100644 --- a/embargo.views.inc +++ b/embargo.views.inc @@ -21,5 +21,19 @@ function embargo_views_data_alter(array &$data) { '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'), + ], + ]; } From 6aaa639601207551161c12373bbf8fbf428a03df Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 18 Apr 2024 15:18:40 -0300 Subject: [PATCH 76/82] Add explicit return of null. Apparently, the implicit return of null breaks the typehint? --- embargo.module | 2 ++ 1 file changed, 2 insertions(+) diff --git a/embargo.module b/embargo.module index b9dec98..7dd6a09 100644 --- a/embargo.module +++ b/embargo.module @@ -47,6 +47,8 @@ function embargo_file_download($uri) : null|array|int { return -1; } } + + return NULL; } /** From 63d01787881f92ee482a4d79673c1e98ee132498 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 9 May 2024 11:51:55 -0300 Subject: [PATCH 77/82] Adjust caching for embargo and related access control. --- src/Access/EmbargoAccessCheck.php | 4 ++++ src/Entity/Embargo.php | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Access/EmbargoAccessCheck.php b/src/Access/EmbargoAccessCheck.php index e643958..3e6dcf0 100644 --- a/src/Access/EmbargoAccessCheck.php +++ b/src/Access/EmbargoAccessCheck.php @@ -74,6 +74,10 @@ public function access(EntityInterface $entity, AccountInterface $user) { )->render() ); array_map([$state, 'addCacheableDependency'], $embargoes); + + $type = $this->entityTypeManager->getDefinition('embargo'); + $state->addCacheTags($type->getListCacheTags()) + ->addCacheContexts($type->getListCacheContexts()); return $state; } diff --git a/src/Entity/Embargo.php b/src/Entity/Embargo.php index 1201540..21d6bdd 100644 --- a/src/Entity/Embargo.php +++ b/src/Entity/Embargo.php @@ -42,7 +42,8 @@ * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider" * }, * }, - * list_cache_tags = { "node_list", "media_list", "file_list" }, + * list_cache_contexts = { "ip.embargo_range", "user" }, + * list_cache_tags = { "embargo_list" }, * base_table = "embargo", * admin_permission = "administer embargo", * entity_keys = { @@ -450,4 +451,18 @@ public function ipIsExempt(string $ip): bool { return $exempt_ips && $exempt_ips->withinRanges($ip); } + /** + * {@inheritdoc} + */ + protected function getListCacheTagsToInvalidate() : array { + return array_merge( + parent::getListCacheTagsToInvalidate(), + [ + 'node_list', + 'media_list', + 'file_list', + ] + ); + } + } From d8c5b3b7d8497cca56b3371b81fe2bf4c735d2be Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Thu, 9 May 2024 13:23:20 -0300 Subject: [PATCH 78/82] Add in the related entity to the mix. --- src/Entity/Embargo.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Embargo.php b/src/Entity/Embargo.php index 21d6bdd..33eca07 100644 --- a/src/Entity/Embargo.php +++ b/src/Entity/Embargo.php @@ -43,7 +43,7 @@ * }, * }, * list_cache_contexts = { "ip.embargo_range", "user" }, - * list_cache_tags = { "embargo_list" }, + * list_cache_tags = { "embargo_list", "embargo_ip_range_list" }, * base_table = "embargo", * admin_permission = "administer embargo", * entity_keys = { From 7f30f6fbb86c1faaa9fce9bda14c5c982f00a04e Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 24 Jun 2024 10:23:19 -0300 Subject: [PATCH 79/82] Attempt to avoid using base `user` cache context. Using the `user` cache context itself effectively prevents caching, so let's try to avoid it except where necessary. --- embargo.services.yml | 7 ++ .../src/Plugin/migrate/source/Entity.php | 4 +- src/Access/EmbargoAccessCheck.php | 21 +++-- .../Context/UserExemptedCacheContext.php | 77 +++++++++++++++++++ src/Entity/Embargo.php | 13 ++-- src/Entity/IpRange.php | 14 +++- .../processor/EmbargoJoinProcessor.php | 2 +- tests/src/Kernel/FileEmbargoTest.php | 2 +- 8 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 src/Cache/Context/UserExemptedCacheContext.php diff --git a/embargo.services.yml b/embargo.services.yml index c6ef73b..c70365d 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -62,3 +62,10 @@ services: class: Drupal\embargo\EventSubscriber\TaggingEventSubscriber tags: - { name: 'event_subscriber' } + cache_context.user.embargo__has_exemption: + class: Drupal\embargo\Cache\Context\IpRangeCacheContext + arguments: + - '@current_user' + - '@entity_type.manager' + tags: + - { name: 'cache.context' } 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 864c45e..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 @@ -40,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); @@ -56,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 3e6dcf0..9af1cfe 100644 --- a/src/Access/EmbargoAccessCheck.php +++ b/src/Access/EmbargoAccessCheck.php @@ -57,28 +57,35 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Req * {@inheritdoc} */ public function access(EntityInterface $entity, AccountInterface $user) { + $type = $this->entityTypeManager->getDefinition('embargo'); + $state = AccessResult::neutral() + ->addCacheTags($type->getListCacheTags()) + ->addCacheContexts($type->getListCacheContexts()); + /** @var \Drupal\embargo\EmbargoStorage $storage */ $storage = $this->entityTypeManager->getStorage('embargo'); + $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); + )); - $type = $this->entityTypeManager->getDefinition('embargo'); - $state->addCacheTags($type->getListCacheTags()) - ->addCacheContexts($type->getListCacheContexts()); - return $state; } } 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/Entity/Embargo.php b/src/Entity/Embargo.php index e67f6ac..89349b8 100644 --- a/src/Entity/Embargo.php +++ b/src/Entity/Embargo.php @@ -12,6 +12,7 @@ use Drupal\Core\TypedData\Exception\MissingDataException; use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface; use Drupal\embargo\EmbargoInterface; +use Drupal\embargo\EmbargoStorageInterface; use Drupal\embargo\IpRangeInterface; use Drupal\node\NodeInterface; use Drupal\user\UserInterface; @@ -43,8 +44,6 @@ * "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider" * }, * }, - * list_cache_contexts = { "ip.embargo_range", "user" }, - * list_cache_tags = { "embargo_list", "embargo_ip_range_list" }, * base_table = "embargo", * admin_permission = "administer embargo", * entity_keys = { @@ -413,7 +412,7 @@ public function getCacheContexts() { $contexts = Cache::mergeContexts( parent::getCacheContexts(), $this->getEmbargoedNode()->getCacheContexts(), - [$this->getExemptUsers() ? 'user' : 'user.permissions'], + ['user.embargo__has_exemption'], ); if ($this->getExemptIps()) { @@ -458,11 +457,9 @@ public function ipIsExempt(string $ip): bool { protected function getListCacheTagsToInvalidate() : array { return array_merge( parent::getListCacheTagsToInvalidate(), - [ - 'node_list', - 'media_list', - 'file_list', - ] + 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 3283ac6..1794c53 100644 --- a/src/Entity/IpRange.php +++ b/src/Entity/IpRange.php @@ -10,6 +10,7 @@ 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; @@ -40,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 = { @@ -250,4 +250,16 @@ public function getCacheContexts() { ]); } + /** + * {@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/Plugin/search_api/processor/EmbargoJoinProcessor.php b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php index 90b8d3f..90f4cf0 100644 --- a/src/Plugin/search_api/processor/EmbargoJoinProcessor.php +++ b/src/Plugin/search_api/processor/EmbargoJoinProcessor.php @@ -289,7 +289,7 @@ public function preprocessSearchQuery(QueryInterface $query) : void { // cacheability. 'ip.embargo_range', // Exemptable users, so need to deal with them. - 'user', + 'user.embargo__has_exemption', ]); // Embargo dates deal with granularity to the day. $query->mergeCacheMaxAge(24 * 3600); 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(); From ab79f893f7a3bcf8be4ca7faccf60bb0047e5930 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Mon, 24 Jun 2024 11:30:05 -0300 Subject: [PATCH 80/82] Account for bypass on individual entities. --- src/Access/EmbargoAccessCheck.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Access/EmbargoAccessCheck.php b/src/Access/EmbargoAccessCheck.php index 9af1cfe..186d4fb 100644 --- a/src/Access/EmbargoAccessCheck.php +++ b/src/Access/EmbargoAccessCheck.php @@ -62,6 +62,11 @@ public function access(EntityInterface $entity, AccountInterface $user) { ->addCacheTags($type->getListCacheTags()) ->addCacheContexts($type->getListCacheContexts()); + 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'); $related_embargoes = $storage->getApplicableEmbargoes($entity); From 7c3d00d83b2a20a33d1436fbf734b1ff6c914360 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Tue, 25 Jun 2024 09:41:55 -0300 Subject: [PATCH 81/82] Move cache contexts around, given permission rules. --- src/Access/EmbargoAccessCheck.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Access/EmbargoAccessCheck.php b/src/Access/EmbargoAccessCheck.php index 186d4fb..e1d0d1d 100644 --- a/src/Access/EmbargoAccessCheck.php +++ b/src/Access/EmbargoAccessCheck.php @@ -58,9 +58,7 @@ public function __construct(EntityTypeManagerInterface $entity_type_manager, Req */ public function access(EntityInterface $entity, AccountInterface $user) { $type = $this->entityTypeManager->getDefinition('embargo'); - $state = AccessResult::neutral() - ->addCacheTags($type->getListCacheTags()) - ->addCacheContexts($type->getListCacheContexts()); + $state = AccessResult::neutral(); if ($user->hasPermission('bypass embargo access')) { return $state->setReason('User has embargo bypass permission.') @@ -69,6 +67,8 @@ public function access(EntityInterface $entity, AccountInterface $user) { /** @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.'); From c0418a64145987460bedec111c8a9d6dc07bce82 Mon Sep 17 00:00:00 2001 From: Adam Vessey Date: Wed, 26 Jun 2024 09:58:56 -0300 Subject: [PATCH 82/82] Reference the correct class. --- embargo.services.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embargo.services.yml b/embargo.services.yml index c70365d..4bcd728 100644 --- a/embargo.services.yml +++ b/embargo.services.yml @@ -63,7 +63,7 @@ services: tags: - { name: 'event_subscriber' } cache_context.user.embargo__has_exemption: - class: Drupal\embargo\Cache\Context\IpRangeCacheContext + class: Drupal\embargo\Cache\Context\UserExemptedCacheContext arguments: - '@current_user' - '@entity_type.manager'