Commit 7b45f117 authored by catch's avatar catch
Browse files

Issue #2958241 by fjgarlin, thetwentyseven, Wim Leers, joachim,...

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

(cherry picked from commit 3f9d9b44)
parent 5aecb66a
Loading
Loading
Loading
Loading
+69 −12
Original line number Diff line number Diff line
@@ -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);
    // 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($node_alias . '.status', 1);
            $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);
  }

}
+13 −1
Original line number Diff line number Diff line
@@ -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');