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