diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml index 0589725..bac290c 100644 --- a/.github/workflows/phpunit.yml +++ b/.github/workflows/phpunit.yml @@ -10,3 +10,10 @@ jobs: PHPUnit: uses: discoverygarden/phpunit-action/.github/workflows/phpunit.yml@v1 secrets: inherit + with: + composer_patches: |- + { + "discoverygarden/islandora_hierarchical_access": { + "dependent work from dependency": "https://github.com/discoverygarden/islandora_hierarchical_access/pull/19.patch" + } + } diff --git a/embargo.module b/embargo.module index 5026ee0..ff0a01e 100644 --- a/embargo.module +++ b/embargo.module @@ -55,25 +55,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 81f9c58..202c158 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 $user; - - /** - * The IP of the request. - * - * @var string - */ - protected $currentIp; - - /** - * Instance of a Drupal database connection. - * - * @var \Drupal\Core\Database\Connection - */ - protected $database; - - /** - * The entity type manager. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface - */ - protected $entityTypeManager; - - /** - * Time service. - * - * @var \Drupal\Component\Datetime\TimeInterface - */ - protected TimeInterface $time; - - /** - * Date formatter service. - * - * @var \Drupal\Core\Datetime\DateFormatterInterface - */ - protected DateFormatterInterface $dateFormatter; + use TaggedTargetsTrait; /** * Constructor. @@ -87,127 +47,30 @@ public function __construct( * * @param \Drupal\Core\Database\Query\SelectInterface $query * The query being executed. - * @param string $type - * Either "node" or "file". */ - public function tagAccess(SelectInterface $query, string $type) { - if (!in_array($type, ['node', 'media', 'file'])) { - throw new \InvalidArgumentException("Unrecognized type '$type'."); + public function tagNode(SelectInterface $query) : void { + if ($query->hasTag('islandora_hierarchical_access_subquery')) { + // Being run as a subquery: We do not want to touch it as we expect our + // IslandoraHierarchicalAccessEventSubscriber class to deal with it. + return; } - elseif ($this->user->hasPermission('bypass embargo access')) { + if ($this->user->hasPermission('bypass embargo access')) { return; } + $type = 'node'; static::conjunctionQuery($query); - /** @var \Drupal\Core\Entity\Sql\SqlEntityStorageInterface $storage */ - $storage = $this->entityTypeManager->getStorage($type); - $tables = $storage->getTableMapping()->getTableNames(); + $tagged_table_aliases = $query->getMetaData('embargo_tagged_table_aliases') ?? []; - foreach ($query->getTables() as $info) { - if ($info['table'] instanceof SelectInterface) { - continue; - } - elseif (in_array($info['table'], $tables)) { - $key = (strpos($info['table'], "{$type}__") === 0) ? 'entity_id' : (substr($type, 0, 1) . "id"); - $alias = $info['alias']; + $target_aliases = $this->getTaggingTargets($query, $tagged_table_aliases, $type); - $to_apply = $query; - if ($info['join type'] == 'LEFT') { - $to_apply = $query->orConditionGroup() - ->isNull("{$alias}.{$key}"); - $query->condition($to_apply); - } - if ($type === 'node') { - $to_apply->condition("{$alias}.{$key}", $this->buildInaccessibleEmbargoesCondition(), 'NOT IN'); - } - elseif ($type === 'media') { - $to_apply->condition("{$alias}.{$key}", $this->buildInaccessibleFileCondition('mid'), 'NOT IN'); - } - elseif ($type === 'file') { - $to_apply->condition("{$alias}.{$key}", $this->buildInaccessibleFileCondition('fid'), 'NOT IN'); - } - else { - throw new \InvalidArgumentException("Invalid type '$type'."); - } - } - } - } - - /** - * Builds the condition for file-typed embargoes that are inaccessible. - * - * @param string $lut_column - * The particular column of the LUT to return, as file embargoes apply to - * media ('mid') as well as files ('fid'). - * - * @return \Drupal\Core\Database\Query\SelectInterface - * The sub-query to be used that results in all file IDs that cannot be - * accessed. - */ - protected function buildInaccessibleFileCondition(string $lut_column) { - $query = $this->database->select('embargo', 'e'); - $lut_alias = $query->join(LUTGeneratorInterface::TABLE_NAME, 'lut', '%alias.nid = e.embargoed_node'); - return $query - ->fields($lut_alias, [$lut_column]) - ->condition('lut.nid', $this->buildAccessibleEmbargoesQuery(EmbargoInterface::EMBARGO_TYPE_FILE), 'NOT IN'); - } - - /** - * Get query to select accessible embargoed entities. - * - * @param int $type - * The type of embargo, expected to be one of: - * - EmbargoInterface::EMBARGO_TYPE_NODE; or, - * - EmbargoInterface::EMBARGO_TYPE_FILE. - * - * @return \Drupal\Core\Database\Query\SelectInterface - * A query returning things that should not be inaccessible. - */ - protected function buildAccessibleEmbargoesQuery($type) : SelectInterface { - $query = $this->database->select('embargo', 'e') - ->fields('e', ['embargoed_node']); - - // Things are visible if... - $group = $query->orConditionGroup() - // The selected embargo entity does not apply to the given type; or... - ->condition('e.embargo_type', $type, '!='); - - $group->condition($query->andConditionGroup() - // ... a scheduled embargo... - ->condition('e.expiration_type', EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) - // ... has a date in the past. - ->condition('e.expiration_date', $this->dateFormatter->format($this->time->getRequestTime(), 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT), '<') - ); - - // ... the incoming IP is in an exempt range; or... - /** @var \Drupal\embargo\IpRangeStorageInterface $storage */ - $storage = $this->entityTypeManager->getStorage('embargo_ip_range'); - $applicable_ip_ranges = $storage->getApplicableIpRanges($this->currentIp); - if (!empty($applicable_ip_ranges)) { - $group->condition('e.exempt_ips', array_keys($applicable_ip_ranges), 'IN'); + if (empty($target_aliases)) { + return; } - // ... the specific user is exempted from the embargo. - $user_alias = $query->leftJoin('embargo__exempt_users', 'u', 'e.id = %alias.entity_id'); - $group->condition("{$user_alias}.exempt_users_target_id", $this->user->id()); - - $query->condition($group); - - return $query; - } - - /** - * Builds the condition for embargoes that are inaccessible. - * - * @return \Drupal\Core\Database\Query\SelectInterface - * The sub-query to be used that results in embargoed_node IDs that - * cannot be accessed. - */ - protected function buildInaccessibleEmbargoesCondition() : SelectInterface { - return $this->database->select('embargo', 'ein') - ->condition('ein.embargoed_node', $this->buildAccessibleEmbargoesQuery(EmbargoInterface::EMBARGO_TYPE_NODE), 'NOT IN') - ->fields('ein', ['embargoed_node']); + $query->addMetaData('embargo_tagged_table_aliases', $tagged_table_aliases); + $this->applyExistenceQuery($query, $target_aliases, [EmbargoInterface::EMBARGO_TYPE_NODE]); } } diff --git a/src/EmbargoExistenceQueryTrait.php b/src/EmbargoExistenceQueryTrait.php new file mode 100644 index 0000000..de6b77c --- /dev/null +++ b/src/EmbargoExistenceQueryTrait.php @@ -0,0 +1,184 @@ +condition( + $existence_condition->orConditionGroup() + ->notExists($this->getNullQuery($target_aliases, $embargo_types)) + ->exists($this->getAccessibleEmbargoesQuery($target_aliases, $embargo_types)) + ); + } + + /** + * Get query for negative assertion. + * + * @param array $target_aliases + * The target aliases on which to match. + * @param array $embargo_types + * The relevant types of embargoes to which to constrain. + * + * @return \Drupal\Core\Database\Query\SelectInterface + * The negative-asserting query. + */ + protected function getNullQuery(array $target_aliases, array $embargo_types) : SelectInterface { + $embargo_alias = 'embargo_null'; + $query = $this->database->select('embargo', $embargo_alias); + $query->addExpression(1, 'embargo_null_e'); + + $query->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; + } + + /** + * Get query for positive assertion. + * + * @param array $target_aliases + * The target aliases on which to match. + * @param array $embargo_types + * The relevant types of embargoes to which to constrain. + * + * @return \Drupal\Core\Database\Query\SelectInterface + * The positive-asserting query. + */ + protected function getAccessibleEmbargoesQuery(array $target_aliases, array $embargo_types) : SelectInterface { + // Embargo exists for the entity, where: + $embargo_alias = 'embargo_existence'; + $embargo_existence = $this->database->select('embargo', $embargo_alias); + $embargo_existence->addExpression(1, 'embargo_allowed'); + + $embargo_existence->addMetaData('embargo_alias', $embargo_alias); + + $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()) + ) + ); + + $embargo_existence->addMetaData('embargo_existence_condition', $existence_condition); + + // - The request IP is exempt. + /** @var \Drupal\embargo\IpRangeStorageInterface $storage */ + $storage = $this->entityTypeManager->getStorage('embargo_ip_range'); + $applicable_ip_ranges = $storage->getApplicableIpRanges($this->currentIp); + if ($applicable_ip_ranges) { + $embargo_or->condition("{$embargo_alias}.exempt_ips", array_keys($applicable_ip_ranges), 'IN'); + } + + // - The user is exempt. + // @todo Should the IP range constraint(s) take precedence? + $user_existence = $this->database->select('embargo__exempt_users', 'eeu'); + $user_existence->addExpression(1, 'user_existence'); + $user_existence->where("eeu.entity_id = {$embargo_alias}.id") + ->condition('eeu.exempt_users_target_id', $this->user->id()); + $embargo_or->exists($user_existence); + + // - There's a scheduled embargo of an appropriate type and no other + // overriding embargo. + $current_date = $this->dateFormatter->format($this->time->getRequestTime(), 'custom', DateTimeItemInterface::DATE_STORAGE_FORMAT); + // No indefinite embargoes or embargoes expiring in the future. + $unexpired_embargo_subquery = $this->database->select('embargo', 'ue') + ->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() + ->condition('ue.expiration_type', EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) + ->condition('ue.expiration_date', $current_date, '>') + ) + ); + + $embargo_or->condition( + $embargo_or->andConditionGroup() + ->condition("{$embargo_alias}.embargo_type", $embargo_types, 'IN') + ->condition("{$embargo_alias}.expiration_type", EmbargoInterface::EXPIRATION_TYPE_SCHEDULED) + ->condition("{$embargo_alias}.expiration_date", $current_date, '<=') + ->notExists($unexpired_embargo_subquery) + ); + + return $embargo_existence; + } + +} diff --git a/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php new file mode 100644 index 0000000..bad9e35 --- /dev/null +++ b/src/EventSubscriber/IslandoraHierarchicalAccessEventSubscriber.php @@ -0,0 +1,97 @@ +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\ConditionInterface $existence_condition */ + $existence_condition = $query->getMetaData('islandora_hierarchical_access_tagged_existence_condition'); + $this->applyExistenceQuery( + $existence_condition, + ['lut_exist.nid'], + match ($event->getType()) { + 'file', 'media' => [ + EmbargoInterface::EMBARGO_TYPE_FILE, + EmbargoInterface::EMBARGO_TYPE_NODE, + ], + 'node' => [EmbargoInterface::EMBARGO_TYPE_NODE], + }, + ); + } + +} diff --git a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php index b053fd8..82c912d 100644 --- a/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php +++ b/tests/src/Kernel/EmbargoAccessQueryTaggingAlterTest.php @@ -3,6 +3,10 @@ namespace Drupal\Tests\embargo\Kernel; use Drupal\embargo\EmbargoInterface; +use Drupal\file\FileInterface; +use Drupal\media\Entity\Media; +use Drupal\media\MediaInterface; +use Drupal\node\NodeInterface; use Drupal\Tests\islandora_test_support\Traits\DatabaseQueryTestTraits; /** @@ -20,6 +24,77 @@ 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; + + /** + * 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} */ @@ -27,11 +102,25 @@ 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->unembargoedNode = $this->createNode(); + $this->unembargoedMedia = $this->createMedia($this->unembargoedFile = $this->createFile(), $this->unembargoedNode); - $this->createNode(); + $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); } /** @@ -42,7 +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.'); + + $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'); } /** @@ -53,7 +146,11 @@ public function testEmbargoNodeQueryAlterAccess() { public function testNodeEmbargoReferencedMediaAccessQueryAlterAccessDenied() { $query = $this->generateMediaSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(0, $result, 'Media of embargoed nodes cannot be viewed'); + + $ids = array_column($result, 'mid'); + $this->assertNotContains($this->embargoedMedia->id(), $ids, 'does not contain embargoed media'); + $this->assertContains($this->unembargoedMedia->id(), $ids, 'contains unembargoed media'); + $this->assertContains($this->unassociatedMedia->id(), $ids, 'contains unassociated media'); } /** @@ -64,7 +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'); + + $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'); } /** @@ -79,7 +181,10 @@ public function testDeletedNodeEmbargoNodeAccessQueryAlterAccessAllowed() { $query = $this->generateNodeSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(2, $result, 'Non embargoed nodes can be viewed'); + $ids = array_column($result, 'nid'); + $this->assertContains($this->embargoedNode->id(), $ids, 'contains formerly embargoed node'); + $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node'); + $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node'); } /** @@ -93,8 +198,11 @@ public function testDeletedNodeEmbargoMediaAccessQueryAlterAccessAllowed() { $this->embargo->delete(); $query = $this->generateMediaSelectAccessQuery($this->user); $result = $query->execute()->fetchAll(); - $this->assertCount(1, $result, - 'Media of non embargoed nodes can be viewed'); + + $ids = array_column($result, 'mid'); + $this->assertContains($this->embargoedMedia->id(), $ids, 'contains formerly embargoed media'); + $this->assertContains($this->unembargoedMedia->id(), $ids, 'contains unembargoed media'); + $this->assertContains($this->unassociatedMedia->id(), $ids, 'contains unassociated media'); } /** @@ -106,11 +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(2, $result, - 'Files of non embargoed nodes can be viewed'); + + $ids = array_column($result, 'fid'); + $this->assertContains($this->embargoedFile->id(), $ids, 'contains formerly embargoed file'); + $this->assertContains($this->unembargoedFile->id(), $ids, 'contains unembargoed file'); + $this->assertContains($this->unassociatedFile->id(), $ids, 'contains unassociated file'); + $this->assertContains($this->mediaTypeDefaultFile->id(), $ids, 'contains default mediatype file'); } /** @@ -119,16 +231,19 @@ public function testDeletedNodeEmbargoFileAccessQueryAlterAccessAllowed() { * @throws \Drupal\Core\Entity\EntityStorageException */ public function testPublishScheduledEmbargoAccess() { - // Create an embargo scheduled to be unpublished in the future. + // Create an embargo scheduled to be published in the future. $this->setEmbargoFutureUnpublishDate($this->embargo); - $nodeCount = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); - $this->assertCount(1, $nodeCount, - 'Node is still embargoed.'); + $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); + + $ids = array_column($result, 'nid'); + $this->assertNotContains($this->embargoedNode->id(), $ids, 'does not contain embargoed node'); + $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node'); + $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node'); } /** - * Tests embargo scheduled to be unpublished in the past. + * Test embargo scheduled in the past, without any other embargo. * * @throws \Drupal\Core\Entity\EntityStorageException */ @@ -137,9 +252,54 @@ public function testUnpublishScheduledEmbargoAccess() { // Create an embargo scheduled to be unpublished in the future. $this->setEmbargoPastUnpublishDate($this->embargo); - $nodeCount = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); - $this->assertCount(2, $nodeCount, - 'Embargo has been unpublished.'); + $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); + + $ids = array_column($result, 'nid'); + $this->assertContains($this->embargoedNode->id(), $ids, 'contains node with expired embargo'); + $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node'); + $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node'); + } + + /** + * Test embargo scheduled in the past with another relevant scheduled embargo. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function testUnpublishScheduledWithPublishedEmbargoAccess() { + $this->embargo->setExpirationType(EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)->save(); + // Create an embargo scheduled to be unpublished in the future. + $this->setEmbargoPastUnpublishDate($this->embargo); + + $embargo = $this->createEmbargo($this->embargoedNode); + $embargo->setExpirationType(EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)->save(); + $this->setEmbargoFutureUnpublishDate($embargo); + + $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); + + $ids = array_column($result, 'nid'); + $this->assertNotContains($this->embargoedNode->id(), $ids, 'does not contain node with expired embargo having other schedule embargo in future'); + $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node'); + $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node'); + } + + /** + * Test embargo scheduled in the past, but with a separate indefinite embargo. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function testUnpublishScheduledWithIndefiniteEmbargoAccess() { + $this->embargo->setExpirationType(EmbargoInterface::EXPIRATION_TYPE_SCHEDULED)->save(); + // Create an embargo scheduled to be unpublished in the future. + $this->setEmbargoPastUnpublishDate($this->embargo); + + $this->createEmbargo($this->embargoedNode); + + $result = $this->generateNodeSelectAccessQuery($this->user)->execute()->fetchAll(); + + $ids = array_column($result, 'nid'); + $this->assertNotContains($this->embargoedNode->id(), $ids, 'does not contain node with expired embargo having other indefinite embargo'); + $this->assertContains($this->unembargoedNode->id(), $ids, 'contains unembargoed node'); + $this->assertContains($this->unassociatedNode->id(), $ids, 'contains unassociated node'); } } diff --git a/tests/src/Kernel/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'); + } + }