From da40cfe5b48551f44c206e0110345920bc887f2c Mon Sep 17 00:00:00 2001
From: Kristiaan Van den Eynde <kristiaan@factorial.io>
Date: Mon, 29 Jun 2020 11:18:35 +0200
Subject: [PATCH] Issue #3086409 by kristiaanvandeneynde: Provide a default
 query_access handler for core (maybe all?) entity types

---
 entity.module                                 | 13 +++
 .../EventOnlyQueryAccessHandler.php           | 84 +++++++++++++++++++
 .../EventSubscriber/QueryAccessSubscriber.php | 18 ++++
 .../EventOnlyQueryAccessHandlerTest.php       | 62 ++++++++++++++
 4 files changed, 177 insertions(+)
 create mode 100644 src/QueryAccess/EventOnlyQueryAccessHandler.php
 create mode 100644 tests/src/Kernel/QueryAccess/EventOnlyQueryAccessHandlerTest.php

diff --git a/entity.module b/entity.module
index a515a47..62f404e 100644
--- a/entity.module
+++ b/entity.module
@@ -67,6 +67,19 @@ function entity_entity_type_build(array &$entity_types) {
   }
 }
 
+/**
+ * Implements hook_entity_type_alter().
+ */
+function entity_entity_type_alter(array &$entity_types) {
+  /** @var $entity_types \Drupal\Core\Entity\EntityTypeInterface[] */
+  // Sets a default query_access handler for all entity types that have none.
+  foreach ($entity_types as $entity_type_id => $entity_type) {
+    if (!$entity_type->hasHandlerClass('query_access')) {
+      $entity_type->setHandlerClass('query_access', 'Drupal\entity\QueryAccess\EventOnlyQueryAccessHandler');
+    }
+  }
+}
+
 /**
  * Implements hook_entity_bundle_info().
  */
diff --git a/src/QueryAccess/EventOnlyQueryAccessHandler.php b/src/QueryAccess/EventOnlyQueryAccessHandler.php
new file mode 100644
index 0000000..c26b749
--- /dev/null
+++ b/src/QueryAccess/EventOnlyQueryAccessHandler.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Entity\EntityHandlerInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Session\AccountInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\EventDispatcher\EventDispatcherInterface;
+
+/**
+ * Generic query access handler for entity types that do not have one.
+ *
+ * This query access handler only fires the alter event so that modules can
+ * subscribe to the query access alter event to alter any entity query or views
+ * query without having to duplicate the related code from Entity API.
+ */
+final class EventOnlyQueryAccessHandler implements EntityHandlerInterface, QueryAccessHandlerInterface {
+
+  /**
+   * The entity type.
+   *
+   * @var \Drupal\Core\Entity\EntityTypeInterface
+   */
+  protected $entityType;
+
+  /**
+   * The event dispatcher.
+   *
+   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
+   */
+  protected $eventDispatcher;
+
+  /**
+   * The current user.
+   *
+   * @var \Drupal\Core\Session\AccountInterface
+   */
+  protected $currentUser;
+
+  /**
+   * Constructs a new EventOnlyQueryAccessHandler object.
+   *
+   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+   *   The entity type.
+   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher
+   *   The event dispatcher.
+   * @param \Drupal\Core\Session\AccountInterface $current_user
+   *   The current user.
+   */
+  public function __construct(EntityTypeInterface $entity_type, EventDispatcherInterface $event_dispatcher, AccountInterface $current_user) {
+    $this->entityType = $entity_type;
+    $this->eventDispatcher = $event_dispatcher;
+    $this->currentUser = $current_user;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
+    return new static(
+      $entity_type,
+      $container->get('event_dispatcher'),
+      $container->get('current_user')
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConditions($operation, AccountInterface $account = NULL) {
+    $account = $account ?: $this->currentUser;
+    $entity_type_id = $this->entityType->id();
+    $conditions = new ConditionGroup('OR');
+
+    // Allow other modules to modify the conditions before they are used.
+    $event = new QueryAccessEvent($conditions, $operation, $account, $entity_type_id);
+    $this->eventDispatcher->dispatch("entity.query_access", $event);
+    $this->eventDispatcher->dispatch("entity.query_access.{$entity_type_id}", $event);
+
+    return $conditions;
+  }
+
+}
diff --git a/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php b/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php
index a6cef32..a36acc1 100644
--- a/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php
+++ b/tests/modules/entity_module_test/src/EventSubscriber/QueryAccessSubscriber.php
@@ -15,6 +15,7 @@ class QueryAccessSubscriber implements EventSubscriberInterface {
     return [
       'entity.query_access' => 'onGenericQueryAccess',
       'entity.query_access.entity_test_enhanced' => 'onQueryAccess',
+      'entity.query_access.node' => 'onEventOnlyQueryAccess',
     ];
   }
 
@@ -81,4 +82,21 @@ class QueryAccessSubscriber implements EventSubscriberInterface {
     }
   }
 
+  /**
+   * Modifies the access conditions based on the node type.
+   *
+   * This is just a convenient example for testing whether the event-only query
+   * access subscriber is added to entity types that do not specify a query
+   * access handler; in this case: node.
+   *
+   * @param \Drupal\entity\QueryAccess\QueryAccessEvent $event
+   *   The event.
+   */
+  public function onEventOnlyQueryAccess(QueryAccessEvent $event) {
+    if (\Drupal::state()->get('test_event_only_query_access')) {
+      $conditions = $event->getConditions();
+      $conditions->addCondition('type', 'foo');
+    }
+  }
+
 }
diff --git a/tests/src/Kernel/QueryAccess/EventOnlyQueryAccessHandlerTest.php b/tests/src/Kernel/QueryAccess/EventOnlyQueryAccessHandlerTest.php
new file mode 100644
index 0000000..9d75169
--- /dev/null
+++ b/tests/src/Kernel/QueryAccess/EventOnlyQueryAccessHandlerTest.php
@@ -0,0 +1,62 @@
+<?php
+
+namespace Drupal\Tests\entity\Kernel\QueryAccess;
+
+use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
+
+/**
+ * Tests the generic query access handler.
+ *
+ * @coversDefaultClass \Drupal\entity\QueryAccess\EventOnlyQueryAccessHandler
+ * @group entity
+ */
+class EventOnlyQueryAccessHandlerTest extends EntityKernelTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static $modules = [
+    'entity',
+    'entity_module_test',
+    'node',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+    $this->installEntitySchema('node');
+
+    // Create uid: 1 here so that it's skipped in test cases.
+    $admin_user = $this->createUser();
+  }
+
+  /**
+   * Tests that entity types without a query access handler still fire events.
+   */
+  public function testEventOnlyQueryAccessHandlerEventSubscriber() {
+    \Drupal::state()->set('test_event_only_query_access', TRUE);
+
+    $node_type_storage = $this->entityTypeManager->getStorage('node_type');
+    $node_type_storage->create(['type' => 'foo', 'name' => $this->randomString()])->save();
+    $node_type_storage->create(['type' => 'bar', 'name' => $this->randomString()])->save();
+
+    $node_storage = $this->entityTypeManager->getStorage('node');
+    $node_1 = $node_storage->create(['type' => 'foo', 'title' => $this->randomString()]);
+    $node_1->save();
+    $node_2 = $node_storage->create(['type' => 'bar', 'title' => $this->randomString()]);
+    $node_2->save();
+
+    $unfiltered = $node_storage->getQuery()->accessCheck(FALSE)->execute();
+    $this->assertCount(2, $unfiltered, 'Both nodes show up when access checking is turned off.');
+    $this->assertArrayHasKey($node_1->id(), $unfiltered, 'foo nodes were not filtered out.');
+    $this->assertArrayHasKey($node_2->id(), $unfiltered, 'bar nodes were not filtered out.');
+
+    $filtered = $node_storage->getQuery()->execute();
+    $this->assertCount(1, $filtered, 'Only one node shows up when access checking is turned on.');
+    $this->assertArrayHasKey($node_1->id(), $filtered, 'foo nodes were not filtered out.');
+    $this->assertArrayNotHasKey($node_2->id(), $filtered, 'bar nodes were filtered out.');
+  }
+
+}
-- 
2.17.1

