From 13955d6bfd72437f56d57b1227362aa58ac58ebd Mon Sep 17 00:00:00 2001
From: Lee Rowlands <lee.rowlands@previousnext.com.au>
Date: Tue, 27 Jun 2023 06:32:41 +1000
Subject: [PATCH] Issue #3332546 by fjgarlin, Berdir, djsagar, larowlan,
 smustgrave: CommentSelection::entityQueryAlter() fails on validate when
 referencing entity is not a comment

---
 .../CommentSelection.php                      |  32 ++++-
 .../Functional/CommentEntityReferenceTest.php | 124 ++++++++++++++++++
 .../src/Kernel/CommentValidationTest.php      | 103 +++++++++++++++
 3 files changed, 257 insertions(+), 2 deletions(-)
 create mode 100644 core/modules/comment/tests/src/Functional/CommentEntityReferenceTest.php

diff --git a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php
index 7446b125904c..e82e2ec3bb57 100644
--- a/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php
+++ b/core/modules/comment/src/Plugin/EntityReferenceSelection/CommentSelection.php
@@ -63,6 +63,27 @@ public function validateReferenceableNewEntities(array $entities) {
     return $entities;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function validateReferenceableEntities(array $ids) {
+    $result = [];
+    if ($ids) {
+      $target_type = $this->configuration['target_type'];
+      $entity_type = $this->entityTypeManager->getDefinition($target_type);
+      $query = $this->buildEntityQuery();
+      // Mirror the conditions checked in buildEntityQuery().
+      if (!$this->currentUser->hasPermission('administer comments')) {
+        $query->condition('status', 1);
+      }
+      $result = $query
+        ->condition($entity_type->getKey('id'), $ids, 'IN')
+        ->execute();
+    }
+
+    return $result;
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -77,9 +98,16 @@ public function entityQueryAlter(SelectInterface $query) {
       $query->innerJoin($data_table, NULL, "[base_table].[cid] = [$data_table].[cid] AND [$data_table].[default_langcode] = 1");
     }
 
-    // Find the host entity type the comment field is on.
+    // Historically, comments were always linked to 'node' entities, but that is
+    // no longer the case, as the 'node' module might not even be enabled.
+    // Comments can now be linked to any entity and they can also be referenced
+    // by other entities, so we won't have a single table to join to. That
+    // actually means that we can no longer optimize the query on those cases.
+    // However, the most common case remains to be comment replies, and in this
+    // case, we can get the host entity type if the 'entity' value is present
+    // and perform the extra joins and alterations needed.
     $comment = $this->getConfiguration()['entity'];
-    if ($comment) {
+    if ($comment instanceof CommentInterface) {
       $host_entity_type_id = $comment->getCommentedEntityTypeId();
 
       /** @var \Drupal\Core\Entity\EntityTypeInterface $host_entity_type */
diff --git a/core/modules/comment/tests/src/Functional/CommentEntityReferenceTest.php b/core/modules/comment/tests/src/Functional/CommentEntityReferenceTest.php
new file mode 100644
index 000000000000..c7018ab61bc5
--- /dev/null
+++ b/core/modules/comment/tests/src/Functional/CommentEntityReferenceTest.php
@@ -0,0 +1,124 @@
+<?php
+
+namespace Drupal\Tests\comment\Functional;
+
+use Drupal\comment\Entity\Comment;
+use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
+
+/**
+ * Tests that comments behave correctly when added as entity references.
+ *
+ * @group comment
+ */
+class CommentEntityReferenceTest extends CommentTestBase {
+
+  use EntityReferenceTestTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * A second test node containing references to comments.
+   *
+   * @var \Drupal\node\NodeInterface
+   */
+  protected $node2;
+
+  /**
+   * A comment linked to a node.
+   *
+   * @var \Drupal\comment\CommentInterface
+   */
+  protected $comment;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp(): void {
+    parent::setUp();
+
+    $this->createEntityReferenceField(
+      'node',
+      'article',
+      'entity_reference_comment',
+      'Entity Reference Comment',
+      'comment',
+      'default',
+      ['target_bundles' => ['comment']]
+    );
+    \Drupal::service('entity_display.repository')
+      ->getFormDisplay('node', 'article')
+      ->setComponent('entity_reference_comment', ['type' => 'options_select'])
+      ->save();
+    \Drupal::service('entity_display.repository')
+      ->getViewDisplay('node', 'article')
+      ->setComponent('entity_reference_comment', ['type' => 'entity_reference_label'])
+      ->save();
+
+    $administratorUser = $this->drupalCreateUser([
+      'skip comment approval',
+      'post comments',
+      'access comments',
+      'access content',
+      'administer nodes',
+      'administer comments',
+      'bypass node access',
+    ]);
+    $this->drupalLogin($administratorUser);
+
+    $this->node = $this->drupalCreateNode(['type' => 'article', 'promote' => 1, 'uid' => $this->webUser->id()]);
+    $this->comment = $this->postComment($this->node, $this->randomMachineName(), $this->randomMachineName());
+    $this->assertInstanceOf(Comment::class, $this->comment);
+
+    $this->node2 = $this->drupalCreateNode([
+      'title' => $this->randomMachineName(),
+      'type' => 'article',
+    ]);
+  }
+
+  /**
+   * Tests that comments are correctly saved as entity references.
+   */
+  public function testCommentAsEntityReference() {
+    // Load the node and save it.
+    $edit = [
+      'entity_reference_comment' => $this->comment->id(),
+    ];
+    $this->drupalGet('node/' . $this->node2->id() . '/edit');
+    $this->submitForm($edit, 'Save');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('has been updated');
+
+    // Make sure the comment is linked.
+    $this->assertSession()->pageTextContains($this->comment->label());
+  }
+
+  /**
+   * Tests that comments of unpublished are not shown.
+   */
+  public function testCommentOfUnpublishedNodeBypassAccess() {
+    // Unpublish the node that has the comment.
+    $this->node->setUnpublished()->save();
+
+    // When the user has 'bypass node access' permission, they can still set it.
+    $edit = [
+      'entity_reference_comment' => $this->comment->id(),
+    ];
+    $this->drupalGet('node/' . $this->node2->id() . '/edit');
+    $this->submitForm($edit, 'Save');
+    $this->assertSession()->statusCodeEquals(200);
+    $this->assertSession()->pageTextContains('has been updated');
+
+    // Comment is seen as administrator user.
+    $this->assertSession()->pageTextContains($this->comment->label());
+
+    // But not as anonymous.
+    $this->drupalLogout();
+    $this->drupalGet('node/' . $this->node2->id());
+    $this->assertSession()->pageTextContains($this->node2->label());
+    $this->assertSession()->pageTextNotContains($this->comment->label());
+  }
+
+}
diff --git a/core/modules/comment/tests/src/Kernel/CommentValidationTest.php b/core/modules/comment/tests/src/Kernel/CommentValidationTest.php
index 254deeed8c29..5f6c23cf1c57 100644
--- a/core/modules/comment/tests/src/Kernel/CommentValidationTest.php
+++ b/core/modules/comment/tests/src/Kernel/CommentValidationTest.php
@@ -3,8 +3,12 @@
 namespace Drupal\Tests\comment\Kernel;
 
 use Drupal\comment\CommentInterface;
+use Drupal\comment\Entity\Comment;
+use Drupal\comment\Entity\CommentType;
+use Drupal\comment\Tests\CommentTestTrait;
 use Drupal\KernelTests\Core\Entity\EntityKernelTestBase;
 use Drupal\node\Entity\Node;
+use Drupal\Tests\field\Traits\EntityReferenceTestTrait;
 use Drupal\user\Entity\User;
 
 /**
@@ -13,6 +17,8 @@
  * @group comment
  */
 class CommentValidationTest extends EntityKernelTestBase {
+  use CommentTestTrait;
+  use EntityReferenceTestTrait;
 
   /**
    * Modules to install.
@@ -27,6 +33,7 @@ class CommentValidationTest extends EntityKernelTestBase {
   protected function setUp(): void {
     parent::setUp();
     $this->installSchema('comment', ['comment_entity_statistics']);
+    $this->installConfig(['comment']);
   }
 
   /**
@@ -180,6 +187,102 @@ public function testValidation() {
     $this->assertEquals('The specified author name does not match the comment author.', $violations[0]->getMessage());
   }
 
+  /**
+   * Tests that comments of unpublished nodes are not valid.
+   */
+  public function testValidationOfCommentOfUnpublishedNode() {
+    // Create a page node type.
+    $this->entityTypeManager->getStorage('node_type')->create([
+      'type' => 'page',
+      'name' => 'page',
+    ])->save();
+
+    // Create a comment type.
+    CommentType::create([
+      'id' => 'comment',
+      'label' => 'Default comments',
+      'description' => 'Default comment field',
+      'target_entity_type_id' => 'node',
+    ])->save();
+
+    // Add comment and entity reference comment fields.
+    $this->addDefaultCommentField('node', 'page', 'comment');
+    $this->createEntityReferenceField(
+      'node',
+      'page',
+      'entity_reference_comment',
+      'Entity Reference Comment',
+      'comment',
+      'default',
+      ['target_bundles' => ['comment']]
+    );
+
+    $comment_admin_user = $this->drupalCreateUser([
+      'skip comment approval',
+      'post comments',
+      'access comments',
+      'access content',
+      'administer nodes',
+      'administer comments',
+      'bypass node access',
+    ]);
+    $comment_non_admin_user = $this->drupalCreateUser([
+      'access comments',
+      'post comments',
+      'create page content',
+      'edit own comments',
+      'skip comment approval',
+      'access content',
+    ]);
+
+    // Create a node with a comment and make it unpublished.
+    $node1 = $this->entityTypeManager->getStorage('node')->create([
+      'type' => 'page',
+      'title' => 'test 1',
+      'promote' => 1,
+      'status' => 0,
+      'uid' => $comment_non_admin_user->id(),
+    ]);
+    $node1->save();
+    $comment1 = $this->entityTypeManager->getStorage('comment')->create([
+      'entity_id' => $node1->id(),
+      'entity_type' => 'node',
+      'field_name' => 'comment',
+      'comment_body' => $this->randomMachineName(),
+    ]);
+    $comment1->save();
+    $this->assertInstanceOf(Comment::class, $comment1);
+
+    // Create a second published node.
+    /** @var \Drupal\node\Entity\Node $node2 */
+    $node2 = $this->entityTypeManager->getStorage('node')->create([
+      'type' => 'page',
+      'title' => 'test 2',
+      'promote' => 1,
+      'status' => 1,
+      'uid' => $comment_non_admin_user->id(),
+    ]);
+    $node2->save();
+
+    // Test the validation API directly.
+    $this->drupalSetCurrentUser($comment_non_admin_user);
+    $this->assertEquals(\Drupal::currentUser()->id(), $comment_non_admin_user->id());
+    $node2->set('entity_reference_comment', $comment1->id());
+    $violations = $node2->validate();
+    $this->assertCount(1, $violations);
+    $this->assertEquals('entity_reference_comment.0.target_id', $violations[0]->getPropertyPath());
+    $this->assertEquals(t('This entity (%type: %name) cannot be referenced.', [
+      '%type' => $comment1->getEntityTypeId(),
+      '%name' => $comment1->id(),
+    ]), $violations[0]->getMessage());
+
+    $this->drupalSetCurrentUser($comment_admin_user);
+    $this->assertEquals(\Drupal::currentUser()->id(), $comment_admin_user->id());
+    $node2->set('entity_reference_comment', $comment1->id());
+    $violations = $node2->validate();
+    $this->assertCount(0, $violations);
+  }
+
   /**
    * Verifies that a length violation exists for the given field.
    *
-- 
GitLab