From 87e10306b55a2e279778d4288d98f7c09fc4155b Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Mon, 21 Nov 2016 20:51:55 +0000
Subject: [PATCH] Issue #2424791 by jibran, dpi, chx, Berdir: Entity query
 hardcodes entity_reference and entity specifier

---
 .../Core/Entity/Query/QueryInterface.php      | 11 ++--
 .../Drupal/Core/Entity/Query/Sql/Tables.php   | 18 +++++--
 .../Entity/EntityQueryRelationshipTest.php    | 54 ++++++++++++++++++-
 3 files changed, 76 insertions(+), 7 deletions(-)

diff --git a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
index 630d1767396c..9c05dfa4d6f2 100644
--- a/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
+++ b/core/lib/Drupal/Core/Entity/Query/QueryInterface.php
@@ -35,13 +35,18 @@ public function getEntityTypeId();
    *
    * @param $field
    *   Name of the field being queried. It must contain a field name, optionally
-   *   followed by a column name. The column can be "entity" for reference
-   *   fields and that can be followed similarly by a field name and so on. Some
-   *   examples:
+   *   followed by a column name. The column can be the reference property,
+   *   usually "entity", for reference fields and that can be followed
+   *   similarly by a field name and so on. Additionally, the target entity type
+   *   can be specified by appending the ":target_entity_type_id" to "entity".
+   *   Some examples:
    *   - nid
    *   - tags.value
    *   - tags
+   *   - tags.entity.name
+   *   - tags.entity:taxonomy_term.name
    *   - uid.entity.name
+   *   - uid.entity:user.name
    *   "tags" "is the same as "tags.value" as value is the default column.
    *   If two or more conditions have the same field names they apply to the
    *   same delta within that field. In order to limit the condition to a
diff --git a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
index 09cea061bddb..e813cde38592 100644
--- a/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Tables.php
@@ -7,6 +7,8 @@
 use Drupal\Core\Entity\Query\QueryException;
 use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
 use Drupal\Core\Entity\Sql\TableMappingInterface;
+use Drupal\Core\Entity\TypedData\EntityDataDefinitionInterface;
+use Drupal\Core\TypedData\DataReferenceDefinitionInterface;
 
 /**
  * Adds tables and fields to the SQL entity query.
@@ -254,10 +256,20 @@ public function addField($field, $type, $langcode) {
           $relationship_specifier = $specifiers[$key + 1];
           $next_index_prefix = $relationship_specifier;
         }
+        $entity_type_id = NULL;
+        // Relationship specifier can also contain the entity type ID, i.e.
+        // entity:node, entity:user or entity:taxonomy.
+        if (strpos($relationship_specifier, ':') !== FALSE) {
+          list($relationship_specifier, $entity_type_id) = explode(':', $relationship_specifier, 2);
+        }
         // Check for a valid relationship.
-        if (isset($propertyDefinitions[$relationship_specifier]) && $field_storage->getPropertyDefinition('entity')->getDataType() == 'entity_reference' ) {
-          // If it is, use the entity type.
-          $entity_type_id = $propertyDefinitions[$relationship_specifier]->getTargetDefinition()->getEntityTypeId();
+        if (isset($propertyDefinitions[$relationship_specifier]) && $propertyDefinitions[$relationship_specifier] instanceof DataReferenceDefinitionInterface) {
+          // If it is, use the entity type if specified already, otherwise use
+          // the definition.
+          $target_definition = $propertyDefinitions[$relationship_specifier]->getTargetDefinition();
+          if (!$entity_type_id && $target_definition instanceof EntityDataDefinitionInterface) {
+            $entity_type_id = $target_definition->getEntityTypeId();
+          }
           $entity_type = $this->entityManager->getDefinition($entity_type_id);
           $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
           // Add the new entity base table using the table and sql column.
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryRelationshipTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryRelationshipTest.php
index 846543820706..60dc13c67b59 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryRelationshipTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityQueryRelationshipTest.php
@@ -1,6 +1,8 @@
 <?php
 
 namespace Drupal\KernelTests\Core\Entity;
+
+use Drupal\Component\Plugin\Exception\PluginNotFoundException;
 use Drupal\Component\Utility\Unicode;
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait;
@@ -115,7 +117,7 @@ protected function setUp() {
    * Tests querying.
    */
   public function testQuery() {
-    // This returns the 0th entity as that's only one pointing to the 0th
+    // This returns the 0th entity as that's the only one pointing to the 0th
     // account.
     $this->queryResults = $this->factory->get('entity_test')
       ->condition("user_id.entity.name", $this->accounts[0]->getUsername())
@@ -154,6 +156,56 @@ public function testQuery() {
       ->condition("$this->fieldName.entity.name", $this->terms[0]->name->value, '<>')
       ->execute();
     $this->assertResults(array(1, 2));
+    // This returns the 0th entity as that's only one pointing to the 0th
+    // account.
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("user_id.entity:user.name", $this->accounts[0]->getUsername())
+      ->execute();
+    $this->assertResults(array(0));
+    // This returns the 1st and 2nd entity as those point to the 1st account.
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("user_id.entity:user.name", $this->accounts[0]->getUsername(), '<>')
+      ->execute();
+    $this->assertResults(array(1, 2));
+    // This returns all three entities because all of them point to an
+    // account.
+    $this->queryResults = $this->factory->get('entity_test')
+      ->exists("user_id.entity:user.name")
+      ->execute();
+    $this->assertResults(array(0, 1, 2));
+    // This returns no entities because all of them point to an account.
+    $this->queryResults = $this->factory->get('entity_test')
+      ->notExists("user_id.entity:user.name")
+      ->execute();
+    $this->assertEqual(count($this->queryResults), 0);
+    // This returns the 0th entity as that's only one pointing to the 0th
+    // term (test without specifying the field column).
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("$this->fieldName.entity:taxonomy_term.name", $this->terms[0]->name->value)
+      ->execute();
+    $this->assertResults(array(0));
+    // This returns the 0th entity as that's only one pointing to the 0th
+    // term (test with specifying the column name).
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("$this->fieldName.target_id.entity:taxonomy_term.name", $this->terms[0]->name->value)
+      ->execute();
+    $this->assertResults(array(0));
+    // This returns the 1st and 2nd entity as those point to the 1st term.
+    $this->queryResults = $this->factory->get('entity_test')
+      ->condition("$this->fieldName.entity:taxonomy_term.name", $this->terms[0]->name->value, '<>')
+      ->execute();
+    $this->assertResults(array(1, 2));
+  }
+
+  /**
+   * Tests the invalid specifier in the query relationship.
+   */
+  public function testInvalidSpecifier() {
+    $this->setExpectedException(PluginNotFoundException::class);
+    $this->factory
+      ->get('taxonomy_term')
+      ->condition('langcode.language.foo', 'bar')
+      ->execute();
   }
 
   /**
-- 
GitLab