From 3f9d9b449fc10a752cd7edc8ec1a7d78faebe68a Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Thu, 14 Jul 2022 23:10:45 +0900
Subject: [PATCH] Issue #2958241 by fjgarlin, thetwentyseven, Wim Leers,
 joachim, immaculatexavier, fulgent, larowlan, lastlink, andypost, nnevill:
 Impossible to reply to comments: commented entity considered unreferencable
 because CommentSelection::entityQueryAlter() joins on {node_field_data} table

---
 .../CommentSelection.php                      | 81 ++++++++++++++++---
 .../src/Functional/CommentNonNodeTest.php     | 14 +++-
 2 files changed, 82 insertions(+), 13 deletions(-)

diff --git a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php
index 2c37f34125fc..7446b125904c 100644
--- a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php
+++ b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\comment\Plugin\EntityReferenceSelection;
 
+use Drupal\Component\Utility\Html;
 use Drupal\Core\Database\Query\SelectInterface;
 use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
 use Drupal\comment\CommentInterface;
@@ -76,19 +77,75 @@ public function entityQueryAlter(SelectInterface $query) {
       $query->innerJoin($data_table, NULL, "[base_table].[cid] = [$data_table].[cid] AND [$data_table].[default_langcode] = 1");
     }
 
-    // The Comment module doesn't implement any proper comment access,
-    // and as a consequence doesn't make sure that comments cannot be viewed
-    // when the user doesn't have access to the node.
-    $node_alias = $query->innerJoin('node_field_data', 'n', "[%alias].[nid] = [$data_table].[entity_id] AND [$data_table].[entity_type] = 'node'");
-    // Pass the query to the node access control.
-    $this->reAlterQuery($query, 'node_access', $node_alias);
-
-    // Passing the query to node_query_node_access_alter() is sadly
-    // insufficient for nodes.
-    // @see \Drupal\node\Plugin\EntityReferenceSelection\NodeSelection::buildEntityQuery()
-    if (!$this->currentUser->hasPermission('bypass node access') && !$this->moduleHandler->hasImplementations('node_grants')) {
-      $query->condition($node_alias . '.status', 1);
+    // Find the host entity type the comment field is on.
+    $comment = $this->getConfiguration()['entity'];
+    if ($comment) {
+      $host_entity_type_id = $comment->getCommentedEntityTypeId();
+
+      /** @var \Drupal\Core\Entity\EntityTypeInterface $host_entity_type */
+      $host_entity_type = $this->entityTypeManager->getDefinition($host_entity_type_id);
+      $host_entity_field_data_table = $host_entity_type->getDataTable();
+
+      // Not all entities have a data table, so check first.
+      if ($host_entity_field_data_table) {
+        $id_key = $host_entity_type->getKey('id');
+
+        // The Comment module doesn't implement per-comment access, so it
+        // checks instead that the user has access to the host entity.
+        $entity_alias = $query->innerJoin($host_entity_field_data_table, 'n', "[%alias].[$id_key] = [$data_table].[entity_id] AND [$data_table].[entity_type] = '$host_entity_type_id'");
+        // Pass the query to the entity access control.
+        $this->reAlterQuery($query, $host_entity_type_id . '_access', $entity_alias);
+
+        // Additional checks for "node" entities.
+        if ($host_entity_type_id === 'node') {
+          // Passing the query to node_query_node_access_alter() is sadly
+          // insufficient for nodes.
+          // @see \Drupal\node\Plugin\EntityReferenceSelection\NodeSelection::buildEntityQuery()
+          if (!$this->currentUser->hasPermission('bypass node access') && !$this->moduleHandler->hasImplementations('node_grants')) {
+            $query->condition($entity_alias . '.status', 1);
+          }
+        }
+      }
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getReferenceableEntities($match = NULL, $match_operator = 'CONTAINS', $limit = 0) {
+    $target_type = $this->getConfiguration()['target_type'];
+
+    $query = $this->buildEntityQuery($match, $match_operator);
+    if ($limit > 0) {
+      $query->range(0, $limit);
     }
+
+    $result = $query->execute();
+
+    if (empty($result)) {
+      return [];
+    }
+
+    $options = [];
+    $entities = $this->entityTypeManager->getStorage($target_type)->loadMultiple($result);
+    foreach ($entities as $entity_id => $entity) {
+      // Additional access check as comments might be attached to entities
+      // which the current user does not have access to.
+      if ($entity->access('view', $this->currentUser)) {
+        $bundle = $entity->bundle();
+        $options[$bundle][$entity_id] = Html::escape($this->entityRepository->getTranslationFromContext($entity)->label() ?? '');
+      }
+    }
+
+    return $options;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function countReferenceableEntities($match = NULL, $match_operator = 'CONTAINS') {
+    $options = $this->getReferenceableEntities($match, $match_operator);
+    return count($options, COUNT_RECURSIVE) - count($options);
   }
 
 }
diff --git a/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php b/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php
index 9820d2553405..faf48498796f 100644
--- a/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php
+++ b/core/modules/comment/tests/src/Functional/CommentNonNodeTest.php
@@ -120,7 +120,7 @@ protected function setUp(): void {
    * @return \Drupal\comment\CommentInterface
    *   The new comment entity.
    */
-  public function postComment(EntityInterface $entity, $comment, $subject = '', $contact = NULL) {
+  public function postComment(?EntityInterface $entity, $comment, $subject = '', $contact = NULL) {
     $edit = [];
     $edit['comment_body[0][value]'] = $comment;
 
@@ -309,6 +309,18 @@ public function testCommentFunctionality() {
     $xpath = '//nav[@aria-labelledby="system-breadcrumb"]/ol/li[last()]/a';
     $this->assertEquals($comment1->getSubject(), current($this->xpath($xpath))->getText(), 'Last breadcrumb item is equal to comment subject on delete confirm page.');
 
+    // Test threading replying to comment #1 creating comment #1_2.
+    $this->drupalGet('comment/reply/entity_test/' . $this->entity->id() . '/comment/' . $comment1->id());
+    $comment1_2 = $this->postComment(NULL, $this->randomMachineName(), $this->randomMachineName());
+    $this->assertTrue($this->commentExists($comment1_2, TRUE), 'Comment #1_2. Reply found.');
+    $this->assertEquals('01.00/', $comment1_2->getThread());
+
+    // Test nested threading replying to comment #1_2 creating comment #1_2_3.
+    $this->drupalGet('comment/reply/entity_test/' . $this->entity->id() . '/comment/' . $comment1_2->id());
+    $comment1_2_3 = $this->postComment(NULL, $this->randomMachineName(), $this->randomMachineName());
+    $this->assertTrue($this->commentExists($comment1_2_3, TRUE), 'Comment #1_2_3. Reply found.');
+    $this->assertEquals('01.00.00/', $comment1_2_3->getThread());
+
     // Unpublish the comment.
     $this->performCommentOperation($comment1, 'unpublish');
     $this->drupalGet('admin/content/comment/approval');
-- 
GitLab