From 4c8c814c1044cb560a069665637b3c1fd8631dc6 Mon Sep 17 00:00:00 2001
From: Dave Long <dave@longwaveconsulting.com>
Date: Fri, 27 Sep 2024 13:29:44 +0200
Subject: [PATCH] Issue #3336994 by mxr576, kksandr, dpi, smustgrave, xjm,
 alexpott, hchonov, quietone: StringFormatter always displays links to entity
 even if the user in context does not have access

---
 .../Field/FieldFormatter/StringFormatter.php  |  17 ++-
 .../Functional/BlockContentListViewsTest.php  |   2 +-
 .../src/Kernel/Views/CommentUserNameTest.php  |  13 ++-
 .../KernelString/StringFormatterTest.php      |  11 +-
 .../Kernel/KernelString/UuidFormatterTest.php |   6 +
 .../FieldFormatter/StringFormatterTest.php    | 110 ++++++++++++++++++
 .../src/Kernel/Views/TaxonomyFieldTidTest.php |   6 +
 .../src/Kernel/Handler/FieldFieldTest.php     |   3 +
 8 files changed, 157 insertions(+), 11 deletions(-)
 create mode 100644 core/modules/field/tests/src/Unit/Plugin/Field/FieldFormatter/StringFormatterTest.php

diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php
index 32f5c20d6ec1..9ccfc05ce9f7 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldFormatter/StringFormatter.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\Core\Field\Plugin\Field\FieldFormatter;
 
+use Drupal\Core\Cache\CacheableMetadata;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Field\Attribute\FieldFormatter;
@@ -122,27 +123,33 @@ public function settingsSummary() {
    */
   public function viewElements(FieldItemListInterface $items, $langcode) {
     $elements = [];
-    $url = NULL;
     $entity = $items->getEntity();
     $entity_type = $entity->getEntityType();
 
+    $render_as_link = FALSE;
     if ($this->getSetting('link_to_entity') && !$entity->isNew() && $entity_type->hasLinkTemplate('canonical')) {
       $url = $this->getEntityUrl($entity);
+      $access = $url->access(return_as_object: TRUE);
+      (new CacheableMetadata())
+        ->addCacheableDependency($access)
+        ->applyTo($elements);
+      $render_as_link = $access->isAllowed();
     }
 
     foreach ($items as $delta => $item) {
-      $view_value = $this->viewValue($item);
-      if ($url) {
+      if ($render_as_link) {
+        assert(isset($url));
         $elements[$delta] = [
           '#type' => 'link',
-          '#title' => $view_value,
+          '#title' => $this->viewValue($item),
           '#url' => $url,
         ];
       }
       else {
-        $elements[$delta] = $view_value;
+        $elements[$delta] = $this->viewValue($item);
       }
     }
+
     return $elements;
   }
 
diff --git a/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php b/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php
index 9c00870e9d3f..0d624cceb690 100644
--- a/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php
+++ b/core/modules/block_content/tests/src/Functional/BlockContentListViewsTest.php
@@ -181,7 +181,7 @@ public function testListing(): void {
     $this->drupalGet('admin/content/block');
     $this->assertSession()->statusCodeEquals(200);
     $this->assertSession()->linkNotExists($link_text);
-    $matches = $this->xpath('//td/a');
+    $matches = $this->xpath('//td[1]');
     $actual = $matches[0]->getText();
     $this->assertEquals($label, $actual, 'Label found for test block.');
     $this->assertSession()->linkNotExists('Edit');
diff --git a/core/modules/comment/tests/src/Kernel/Views/CommentUserNameTest.php b/core/modules/comment/tests/src/Kernel/Views/CommentUserNameTest.php
index 02b2eb0b23c7..e0b88a22e3f0 100644
--- a/core/modules/comment/tests/src/Kernel/Views/CommentUserNameTest.php
+++ b/core/modules/comment/tests/src/Kernel/Views/CommentUserNameTest.php
@@ -56,7 +56,11 @@ protected function setUp($import_test_views = TRUE): void {
 
     $admin_role = Role::create([
       'id' => 'admin',
-      'permissions' => ['administer comments', 'access user profiles'],
+      'permissions' => [
+        'administer comments',
+        'access user profiles',
+        'access comments',
+      ],
       'label' => 'Admin',
     ]);
     $admin_role->save();
@@ -177,8 +181,11 @@ public function testUsername(): void {
     $this->assertNoLink($this->adminUser->label());
     // Note: External users aren't pointing to drupal user profiles.
     $this->assertLink('barry (not verified)');
-    $this->assertLink('My comment title');
-    $this->assertLink('Anonymous comment title');
+    // Anonymous user does not have access to this link but can still see title.
+    $this->assertText('My comment title');
+    $this->assertNoLink('My comment title');
+    $this->assertText('Anonymous comment title');
+    $this->assertNoLink('Anonymous comment title');
   }
 
 }
diff --git a/core/modules/field/tests/src/Kernel/KernelString/StringFormatterTest.php b/core/modules/field/tests/src/Kernel/KernelString/StringFormatterTest.php
index 4c8b4027a510..a591e3cccbad 100644
--- a/core/modules/field/tests/src/Kernel/KernelString/StringFormatterTest.php
+++ b/core/modules/field/tests/src/Kernel/KernelString/StringFormatterTest.php
@@ -12,6 +12,7 @@
 use Drupal\field\Entity\FieldConfig;
 use Drupal\field\Entity\FieldStorageConfig;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\user\Traits\UserCreationTrait;
 
 /**
  * Tests the creation of text fields.
@@ -20,6 +21,8 @@
  */
 class StringFormatterTest extends KernelTestBase {
 
+  use UserCreationTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -68,7 +71,10 @@ protected function setUp(): void {
     // Configure the theme system.
     $this->installConfig(['system', 'field']);
     $this->installEntitySchema('entity_test_rev');
-    $this->installEntitySchema('entity_test_label');
+    $this->setUpCurrentUser(permissions: [
+      'view test entity',
+      'administer entity_test content',
+    ]);
 
     $this->entityType = 'entity_test_rev';
     $this->bundle = $this->entityType;
@@ -124,7 +130,7 @@ public function testStringFormatter(): void {
     $value .= "\n\n<strong>" . $this->randomString() . '</strong>';
     $value .= "\n\n" . $this->randomString();
 
-    $entity = EntityTestRev::create([]);
+    $entity = EntityTestRev::create(['name' => 'view revision']);
     $entity->{$this->fieldName}->value = $value;
 
     // Verify that all HTML is escaped and newlines are retained.
@@ -194,6 +200,7 @@ public function testStringFormatter(): void {
    */
   public function testLinkToContentForEntitiesWithNoCanonicalPath(): void {
     $this->enableModules(['entity_test']);
+    $this->installEntitySchema('entity_test_label');
     $field_name = 'test_field_name';
     $entity_type = $bundle = 'entity_test_label';
 
diff --git a/core/modules/field/tests/src/Kernel/KernelString/UuidFormatterTest.php b/core/modules/field/tests/src/Kernel/KernelString/UuidFormatterTest.php
index 736d64626721..1defb4452ab3 100644
--- a/core/modules/field/tests/src/Kernel/KernelString/UuidFormatterTest.php
+++ b/core/modules/field/tests/src/Kernel/KernelString/UuidFormatterTest.php
@@ -6,6 +6,7 @@
 
 use Drupal\entity_test\Entity\EntityTest;
 use Drupal\KernelTests\KernelTestBase;
+use Drupal\Tests\user\Traits\UserCreationTrait;
 
 /**
  * Tests the output of a UUID field.
@@ -14,6 +15,7 @@
  */
 class UuidFormatterTest extends KernelTestBase {
 
+  use UserCreationTrait;
 
   /**
    * {@inheritdoc}
@@ -28,6 +30,10 @@ protected function setUp(): void {
 
     $this->installConfig(['system', 'field']);
     $this->installEntitySchema('entity_test');
+    $this->installEntitySchema('user');
+    $this->setUpCurrentUser(permissions: [
+      'view test entity',
+    ]);
   }
 
   /**
diff --git a/core/modules/field/tests/src/Unit/Plugin/Field/FieldFormatter/StringFormatterTest.php b/core/modules/field/tests/src/Unit/Plugin/Field/FieldFormatter/StringFormatterTest.php
new file mode 100644
index 000000000000..c7a9f21826ff
--- /dev/null
+++ b/core/modules/field/tests/src/Unit/Plugin/Field/FieldFormatter/StringFormatterTest.php
@@ -0,0 +1,110 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\field\Unit\Plugin\Field\FieldFormatter;
+
+use Drupal\Core\Access\AccessResultAllowed;
+use Drupal\Core\Access\AccessResultForbidden;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
+use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\Plugin\Field\FieldFormatter\StringFormatter;
+use Drupal\Core\Field\Plugin\Field\FieldType\StringItem;
+use Drupal\Core\Url;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * Tests the string field formatter.
+ *
+ * @group field
+ *
+ * @coversDefaultClass \Drupal\Core\Field\Plugin\Field\FieldFormatter\StringFormatter
+ */
+final class StringFormatterTest extends UnitTestCase {
+
+  /**
+   * Checks link visibility depending on link templates and access.
+   *
+   * @param bool $hasUrl
+   *   Whether the entity type has a canonical link template.
+   * @param string|null $accessClass
+   *   The access result for the current user.
+   * @param bool $expectIsLinkElement
+   *   Whether to expect the text to be wrapped in a link element.
+   *
+   * @phpstan-param class-string<\Drupal\Core\Access\AccessResultInterface>|null $accessClass
+   *
+   * @dataProvider providerAccessLinkToEntity
+   */
+  public function testLinkToEntity(bool $hasUrl, ?string $accessClass, bool $expectIsLinkElement): void {
+    $fieldDefinition = $this->prophesize(FieldDefinitionInterface::class);
+    $entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
+    $fieldFormatter = new StringFormatter('foobar', [], $fieldDefinition->reveal(), [], 'TestLabel', 'default', [], $entityTypeManager->reveal());
+    $fieldFormatter->setSetting('link_to_entity', TRUE);
+
+    $entityType = $this->prophesize(EntityTypeInterface::class);
+    $entityType->hasLinkTemplate('canonical')->willReturn($hasUrl)->shouldBeCalledTimes(1);
+    $entityType->hasLinkTemplate('revision')->willReturn(FALSE)->shouldBeCalledTimes($hasUrl ? 1 : 0);
+
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->isNew()->willReturn(FALSE);
+    $entity->getEntityType()->willReturn($entityType->reveal());
+    if ($hasUrl) {
+      $url = $this->prophesize(Url::class);
+      $url->access(NULL, TRUE)->willReturn(new $accessClass());
+      $entity->toUrl('canonical')->willReturn($url);
+    }
+
+    $item = $this->getMockBuilder(StringItem::class)
+      ->disableOriginalConstructor()
+      ->onlyMethods([])
+      ->getMock();
+    $item->setValue(['value' => 'FooText']);
+
+    $items = $this->prophesize(FieldItemListInterface::class);
+    $items->getEntity()->willReturn($entity->reveal());
+    $items->valid()->willReturn(TRUE, FALSE);
+    $items->next();
+    $items->rewind();
+    $items->current()->willReturn($item);
+    $items->key()->willReturn(0);
+
+    $elements = $fieldFormatter->viewElements($items->reveal(), 'en');
+    if ($expectIsLinkElement) {
+      $this->assertEquals('link', $elements[0]['#type']);
+      $this->assertEquals('FooText', $elements[0]['#title']['#context']['value']);
+    }
+    else {
+      $this->assertEquals('inline_template', $elements[0]['#type']);
+      $this->assertEquals('FooText', $elements[0]['#context']['value']);
+    }
+  }
+
+  /**
+   * Data provider.
+   *
+   * @return \Generator
+   *   Test scenarios.
+   */
+  public static function providerAccessLinkToEntity(): \Generator {
+    yield 'entity with no URL' => [
+      FALSE,
+      NULL,
+      FALSE,
+    ];
+    yield 'entity with url, with access' => [
+      TRUE,
+      AccessResultAllowed::class,
+      TRUE,
+    ];
+    yield 'entity with url, no access' => [
+      TRUE,
+      AccessResultForbidden::class,
+      FALSE,
+    ];
+  }
+
+}
diff --git a/core/modules/taxonomy/tests/src/Kernel/Views/TaxonomyFieldTidTest.php b/core/modules/taxonomy/tests/src/Kernel/Views/TaxonomyFieldTidTest.php
index fd3037bd3abc..d66a211b5728 100644
--- a/core/modules/taxonomy/tests/src/Kernel/Views/TaxonomyFieldTidTest.php
+++ b/core/modules/taxonomy/tests/src/Kernel/Views/TaxonomyFieldTidTest.php
@@ -7,6 +7,7 @@
 use Drupal\Core\Link;
 use Drupal\Core\Render\RenderContext;
 use Drupal\Tests\taxonomy\Traits\TaxonomyTestTrait;
+use Drupal\Tests\user\Traits\UserCreationTrait;
 use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
 use Drupal\views\Tests\ViewTestData;
 use Drupal\views\Views;
@@ -19,6 +20,7 @@
 class TaxonomyFieldTidTest extends ViewsKernelTestBase {
 
   use TaxonomyTestTrait;
+  use UserCreationTrait;
 
   /**
    * {@inheritdoc}
@@ -49,7 +51,11 @@ protected function setUp($import_test_views = TRUE): void {
     parent::setUp($import_test_views);
 
     $this->installEntitySchema('taxonomy_term');
+    $this->installEntitySchema('user');
     $this->installConfig(['filter']);
+    $this->setUpCurrentUser(permissions: [
+      'access content',
+    ]);
 
     /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary */
     $vocabulary = $this->createVocabulary();
diff --git a/core/modules/views/tests/src/Kernel/Handler/FieldFieldTest.php b/core/modules/views/tests/src/Kernel/Handler/FieldFieldTest.php
index e57953228cdc..bb07cf3453cc 100644
--- a/core/modules/views/tests/src/Kernel/Handler/FieldFieldTest.php
+++ b/core/modules/views/tests/src/Kernel/Handler/FieldFieldTest.php
@@ -333,6 +333,9 @@ public function testFieldAlias(): void {
    * different delta limit / offset.
    */
   public function testFieldAliasRender(): void {
+    $this->setUpCurrentUser(permissions: [
+      'view test entity',
+    ]);
     $executable = Views::getView('test_field_alias_test');
     $executable->execute();
 
-- 
GitLab